mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
fixes
This commit is contained in:
parent
1023edd66b
commit
04efb37a1f
@ -19,6 +19,7 @@ public class RequestUriUtils {
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".ico")
|
||||
|| requestURI.endsWith(".txt")
|
||||
|| requestURI.endsWith(".webmanifest")
|
||||
|| requestURI.startsWith(contextPath + "/api/v1/info/status");
|
||||
}
|
||||
@ -35,6 +36,7 @@ public class RequestUriUtils {
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".ico")
|
||||
|| requestURI.endsWith(".css")
|
||||
|| requestURI.endsWith(".txt")
|
||||
|| requestURI.endsWith(".map")
|
||||
|| requestURI.endsWith(".svg")
|
||||
|| requestURI.endsWith("popularity.txt")
|
||||
|
@ -13,6 +13,11 @@ import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import stirling.software.common.util.RequestUriUtils;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.service.AuditService;
|
||||
|
||||
@ -95,6 +100,14 @@ public class ControllerAuditAspect {
|
||||
// Get the request path
|
||||
String path = getRequestPath(method, httpMethod);
|
||||
|
||||
// Skip auditing static resources for GET requests
|
||||
if ("GET".equals(httpMethod)) {
|
||||
HttpServletRequest request = getCurrentRequest();
|
||||
if (request != null && RequestUriUtils.isStaticResource(request.getContextPath(), request.getRequestURI())) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
}
|
||||
|
||||
// Create audit data
|
||||
Map<String, Object> auditData = new HashMap<>();
|
||||
auditData.put("controller", joinPoint.getTarget().getClass().getSimpleName());
|
||||
@ -246,4 +259,12 @@ public class ControllerAuditAspect {
|
||||
// Combine base path and method path
|
||||
return basePath + methodPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current HttpServletRequest from the RequestContextHolder
|
||||
*/
|
||||
private HttpServletRequest getCurrentRequest() {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return attributes != null ? attributes.getRequest() : null;
|
||||
}
|
||||
}
|
@ -5,9 +5,12 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
@ -29,8 +32,10 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.proprietary.audit.AuditEventType;
|
||||
import stirling.software.proprietary.audit.AuditLevel;
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
@ -64,6 +69,9 @@ public class AuditDashboardController {
|
||||
// Add audit level enum values for display
|
||||
model.addAttribute("auditLevels", AuditLevel.values());
|
||||
|
||||
// Add audit event types for the dropdown
|
||||
model.addAttribute("auditEventTypes", AuditEventType.values());
|
||||
|
||||
return "audit/dashboard";
|
||||
}
|
||||
|
||||
@ -73,56 +81,75 @@ public class AuditDashboardController {
|
||||
@GetMapping("/data")
|
||||
@ResponseBody
|
||||
public Map<String, Object> getAuditData(
|
||||
@RequestParam(value = "page", defaultValue = "0") int page,
|
||||
@RequestParam(value = "size", defaultValue = "20") int size,
|
||||
@RequestParam(value = "page", defaultValue = "0") Long page,
|
||||
@RequestParam(value = "size", defaultValue = "30") Long size,
|
||||
@RequestParam(value = "type", required = false) String type,
|
||||
@RequestParam(value = "principal", required = false) String principal,
|
||||
@RequestParam(value = "startDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam(value = "endDate", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("timestamp").descending());
|
||||
|
||||
// Create dynamic query based on parameters
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, HttpServletRequest request) {
|
||||
|
||||
log.info("Raw query string: {}", request.getQueryString());
|
||||
|
||||
Pageable pageable = PageRequest.of(page.intValue(), size.intValue(), Sort.by("timestamp").descending());
|
||||
Page<PersistentAuditEvent> events;
|
||||
|
||||
|
||||
String mode = "unknown";
|
||||
|
||||
if (type != null && principal != null && startDate != null && endDate != null) {
|
||||
mode = "principal + type + startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByPrincipalAndTypeAndTimestampBetween(
|
||||
principal, type, start, end, pageable);
|
||||
events = auditRepository.findByPrincipalAndTypeAndTimestampBetween(principal, type, start, end, pageable);
|
||||
} else if (type != null && principal != null) {
|
||||
mode = "principal + type";
|
||||
events = auditRepository.findByPrincipalAndType(principal, type, pageable);
|
||||
} else if (type != null && startDate != null && endDate != null) {
|
||||
mode = "type + startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByTypeAndTimestampBetween(type, start, end, pageable);
|
||||
} else if (principal != null && startDate != null && endDate != null) {
|
||||
mode = "principal + startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByPrincipalAndTimestampBetween(principal, start, end, pageable);
|
||||
} else if (startDate != null && endDate != null) {
|
||||
mode = "startDate + endDate";
|
||||
Instant start = startDate.atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByTimestampBetween(start, end, pageable);
|
||||
} else if (type != null) {
|
||||
mode = "type";
|
||||
events = auditRepository.findByType(type, pageable);
|
||||
} else if (principal != null) {
|
||||
mode = "principal";
|
||||
events = auditRepository.findByPrincipal(principal, pageable);
|
||||
} else {
|
||||
mode = "all";
|
||||
events = auditRepository.findAll(pageable);
|
||||
}
|
||||
|
||||
// Format the response
|
||||
|
||||
// Logging
|
||||
List<PersistentAuditEvent> content = events.getContent();
|
||||
Long firstId = content.isEmpty() ? null : content.get(0).getId();
|
||||
Long lastId = content.isEmpty() ? null : content.get(content.size() - 1).getId();
|
||||
|
||||
log.info("Audit request: page={} size={} mode='{}' → result page={} totalElements={} totalPages={} contentSize={}",
|
||||
page, size, mode, events.getNumber(), events.getTotalElements(), events.getTotalPages(), content.size());
|
||||
|
||||
log.info("Audit content ID range: firstId={} lastId={} (descending timestamp)", firstId, lastId);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("content", events.getContent());
|
||||
response.put("content", content);
|
||||
response.put("totalPages", events.getTotalPages());
|
||||
response.put("totalElements", events.getTotalElements());
|
||||
response.put("currentPage", events.getNumber());
|
||||
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get statistics for charts.
|
||||
@ -160,6 +187,31 @@ public class AuditDashboardController {
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique event types from the database for filtering.
|
||||
*/
|
||||
@GetMapping("/types")
|
||||
@ResponseBody
|
||||
public List<String> getAuditTypes() {
|
||||
// Get distinct event types from the database
|
||||
List<Object[]> results = auditRepository.findDistinctEventTypes();
|
||||
List<String> dbTypes = results.stream()
|
||||
.map(row -> (String) row[0])
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Include standard enum types in case they're not in the database yet
|
||||
List<String> enumTypes = Arrays.stream(AuditEventType.values())
|
||||
.map(AuditEventType::name)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Combine both sources, remove duplicates, and sort
|
||||
Set<String> combinedTypes = new HashSet<>();
|
||||
combinedTypes.addAll(dbTypes);
|
||||
combinedTypes.addAll(enumTypes);
|
||||
|
||||
return combinedTypes.stream().sorted().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Export audit data as CSV.
|
||||
*/
|
||||
@ -195,7 +247,7 @@ public class AuditDashboardController {
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByTimestampBetween(start, end);
|
||||
} else if (type != null) {
|
||||
events = auditRepository.findByType(type);
|
||||
events = auditRepository.findByTypeForExport(type);
|
||||
} else if (principal != null) {
|
||||
events = auditRepository.findByPrincipal(principal);
|
||||
} else {
|
||||
@ -263,7 +315,7 @@ public class AuditDashboardController {
|
||||
Instant end = endDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant();
|
||||
events = auditRepository.findByTimestampBetween(start, end);
|
||||
} else if (type != null) {
|
||||
events = auditRepository.findByType(type);
|
||||
events = auditRepository.findByTypeForExport(type);
|
||||
} else if (principal != null) {
|
||||
events = auditRepository.findByPrincipal(principal);
|
||||
} else {
|
||||
|
@ -67,7 +67,7 @@ public class AuditExampleController {
|
||||
/**
|
||||
* Example using @Audited annotation with file upload at VERBOSE level
|
||||
*/
|
||||
@Audited(type = AuditEventType.FILE_DOWNLOAD, level = AuditLevel.VERBOSE, includeResult = true)
|
||||
@Audited(type = AuditEventType.FILE_OPERATION, level = AuditLevel.VERBOSE, includeResult = true)
|
||||
@PostMapping("/files/process")
|
||||
public ResponseEntity<Map<String, Object>> processFile(MultipartFile file) {
|
||||
// This method is automatically audited at VERBOSE level
|
||||
|
@ -7,6 +7,7 @@ import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import stirling.software.proprietary.model.security.PersistentAuditEvent;
|
||||
@ -26,7 +27,8 @@ public interface PersistentAuditEventRepository
|
||||
|
||||
// Non-paged versions for export
|
||||
List<PersistentAuditEvent> findByPrincipal(String principal);
|
||||
List<PersistentAuditEvent> findByType(String type);
|
||||
@Query("SELECT e FROM PersistentAuditEvent e WHERE e.type = :type")
|
||||
List<PersistentAuditEvent> findByTypeForExport(@Param("type") String type);
|
||||
List<PersistentAuditEvent> findByTimestampBetween(Instant startDate, Instant endDate);
|
||||
List<PersistentAuditEvent> findByTimestampAfter(Instant startDate);
|
||||
List<PersistentAuditEvent> findByPrincipalAndType(String principal, String type);
|
||||
@ -50,4 +52,8 @@ public interface PersistentAuditEventRepository
|
||||
|
||||
@Query("SELECT e.principal, COUNT(e) FROM PersistentAuditEvent e GROUP BY e.principal")
|
||||
List<Object[]> countByPrincipal();
|
||||
|
||||
// Get distinct event types for filtering
|
||||
@Query("SELECT DISTINCT e.type FROM PersistentAuditEvent e ORDER BY e.type")
|
||||
List<Object[]> findDistinctEventTypes();
|
||||
}
|
140
proprietary/src/main/resources/static/css/audit-dashboard.css
Normal file
140
proprietary/src/main/resources/static/css/audit-dashboard.css
Normal file
@ -0,0 +1,140 @@
|
||||
.dashboard-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
.filter-card {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.level-indicator {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
.level-0 {
|
||||
background-color: #dc3545; /* Red */
|
||||
}
|
||||
.level-1 {
|
||||
background-color: #fd7e14; /* Orange */
|
||||
}
|
||||
.level-2 {
|
||||
background-color: #28a745; /* Green */
|
||||
}
|
||||
.level-3 {
|
||||
background-color: #17a2b8; /* Teal */
|
||||
}
|
||||
/* Custom data table styling */
|
||||
.audit-table {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.audit-table th {
|
||||
background-color: #f8f9fa;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.table-responsive {
|
||||
max-height: 600px;
|
||||
}
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
background-color: var(--bs-primary);
|
||||
border-color: var(--bs-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.pagination .page-link.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--bs-secondary);
|
||||
background-color: var(--bs-light);
|
||||
}
|
||||
.json-viewer {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Simple, minimal radio styling - no extras */
|
||||
.form-check {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
#debug-console {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 400px;
|
||||
height: 200px;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: #0f0;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border: 1px solid #0f0;
|
||||
display: none; /* Changed to none by default, enable with key command */
|
||||
}
|
||||
|
||||
/* Enhanced styling for radio buttons as buttons */
|
||||
label.btn-outline-primary {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
label.btn-outline-primary.active {
|
||||
background-color: var(--bs-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
label.btn-outline-primary input[type="radio"] {
|
||||
cursor: pointer;
|
||||
}
|
735
proprietary/src/main/resources/static/js/audit/dashboard.js
Normal file
735
proprietary/src/main/resources/static/js/audit/dashboard.js
Normal file
@ -0,0 +1,735 @@
|
||||
// Initialize variables
|
||||
let currentPage = 0;
|
||||
let pageSize = 20;
|
||||
let totalPages = 0;
|
||||
let typeFilter = '';
|
||||
let principalFilter = '';
|
||||
let startDateFilter = '';
|
||||
let endDateFilter = '';
|
||||
|
||||
// Charts
|
||||
let typeChart;
|
||||
let userChart;
|
||||
let timeChart;
|
||||
|
||||
// DOM elements - will properly initialize these during page load
|
||||
let auditTableBody;
|
||||
let pageSizeSelect;
|
||||
let typeFilterInput;
|
||||
let exportTypeFilterInput;
|
||||
let principalFilterInput;
|
||||
let startDateFilterInput;
|
||||
let endDateFilterInput;
|
||||
let applyFiltersButton;
|
||||
let resetFiltersButton;
|
||||
|
||||
// Debug logger function
|
||||
let debugEnabled = false; // Set to false by default in production
|
||||
function debugLog(message, data) {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
const console = document.getElementById('debug-console');
|
||||
if (console) {
|
||||
if (console.style.display === 'none' || !console.style.display) {
|
||||
console.style.display = 'block';
|
||||
}
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
let logMessage = `[${time}] ${message}`;
|
||||
|
||||
if (data !== undefined) {
|
||||
if (typeof data === 'object') {
|
||||
try {
|
||||
logMessage += ': ' + JSON.stringify(data);
|
||||
} catch (e) {
|
||||
logMessage += ': ' + data;
|
||||
}
|
||||
} else {
|
||||
logMessage += ': ' + data;
|
||||
}
|
||||
}
|
||||
|
||||
const logLine = document.createElement('div');
|
||||
logLine.textContent = logMessage;
|
||||
console.appendChild(logLine);
|
||||
console.scrollTop = console.scrollHeight;
|
||||
|
||||
// Keep only last 100 lines
|
||||
while (console.childNodes.length > 100) {
|
||||
console.removeChild(console.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Also log to browser console
|
||||
if (data !== undefined) {
|
||||
window.console.log(message, data);
|
||||
} else {
|
||||
window.console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Add keyboard shortcut to toggle debug console
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'F12' && e.ctrlKey) {
|
||||
const console = document.getElementById('debug-console');
|
||||
if (console) {
|
||||
console.style.display = console.style.display === 'none' || !console.style.display ? 'block' : 'none';
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
debugLog('Page initialized');
|
||||
|
||||
// Initialize DOM references
|
||||
auditTableBody = document.getElementById('auditTableBody');
|
||||
pageSizeSelect = document.getElementById('pageSizeSelect');
|
||||
typeFilterInput = document.getElementById('typeFilter');
|
||||
exportTypeFilterInput = document.getElementById('exportTypeFilter');
|
||||
principalFilterInput = document.getElementById('principalFilter');
|
||||
startDateFilterInput = document.getElementById('startDateFilter');
|
||||
endDateFilterInput = document.getElementById('endDateFilter');
|
||||
applyFiltersButton = document.getElementById('applyFilters');
|
||||
resetFiltersButton = document.getElementById('resetFilters');
|
||||
|
||||
// Debug log DOM elements
|
||||
debugLog('DOM elements initialized', {
|
||||
auditTableBody: !!auditTableBody,
|
||||
pageSizeSelect: !!pageSizeSelect
|
||||
});
|
||||
|
||||
// Load event types for dropdowns
|
||||
loadEventTypes();
|
||||
|
||||
// Show a loading message immediately
|
||||
if (auditTableBody) {
|
||||
auditTableBody.innerHTML =
|
||||
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> Loading audit data...</td></tr>';
|
||||
} else {
|
||||
debugLog('ERROR: auditTableBody element not found!');
|
||||
}
|
||||
|
||||
// Make a direct API call first to avoid validation issues
|
||||
loadAuditData(0, pageSize);
|
||||
|
||||
// Load statistics for dashboard
|
||||
loadStats(7);
|
||||
|
||||
// Set up event listeners
|
||||
pageSizeSelect.addEventListener('change', function() {
|
||||
pageSize = parseInt(this.value);
|
||||
window.originalPageSize = pageSize;
|
||||
currentPage = 0;
|
||||
window.requestedPage = 0;
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
applyFiltersButton.addEventListener('click', function() {
|
||||
typeFilter = typeFilterInput.value.trim();
|
||||
principalFilter = principalFilterInput.value.trim();
|
||||
startDateFilter = startDateFilterInput.value;
|
||||
endDateFilter = endDateFilterInput.value;
|
||||
currentPage = 0;
|
||||
window.requestedPage = 0;
|
||||
debugLog('Applying filters and resetting to page 0');
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
resetFiltersButton.addEventListener('click', function() {
|
||||
// Reset input fields
|
||||
typeFilterInput.value = '';
|
||||
principalFilterInput.value = '';
|
||||
startDateFilterInput.value = '';
|
||||
endDateFilterInput.value = '';
|
||||
|
||||
// Reset filter variables
|
||||
typeFilter = '';
|
||||
principalFilter = '';
|
||||
startDateFilter = '';
|
||||
endDateFilter = '';
|
||||
|
||||
// Reset page
|
||||
currentPage = 0;
|
||||
window.requestedPage = 0;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('currentPage').textContent = '1';
|
||||
|
||||
debugLog('Resetting filters and going to page 0');
|
||||
|
||||
// Load data with reset filters
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
// Reset export filters button
|
||||
document.getElementById('resetExportFilters').addEventListener('click', function() {
|
||||
exportTypeFilter.value = '';
|
||||
exportPrincipalFilter.value = '';
|
||||
exportStartDateFilter.value = '';
|
||||
exportEndDateFilter.value = '';
|
||||
});
|
||||
|
||||
// Make radio buttons behave like toggle buttons
|
||||
const radioLabels = document.querySelectorAll('label.btn-outline-primary');
|
||||
radioLabels.forEach(label => {
|
||||
const radio = label.querySelector('input[type="radio"]');
|
||||
|
||||
if (radio) {
|
||||
// Highlight the checked radio button's label
|
||||
if (radio.checked) {
|
||||
label.classList.add('active');
|
||||
}
|
||||
|
||||
// Handle clicking on the label
|
||||
label.addEventListener('click', function() {
|
||||
// Remove active class from all labels
|
||||
radioLabels.forEach(l => l.classList.remove('active'));
|
||||
|
||||
// Add active class to this label
|
||||
this.classList.add('active');
|
||||
|
||||
// Check this radio button
|
||||
radio.checked = true;
|
||||
|
||||
debugLog('Radio format selected', radio.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle export button with debug
|
||||
exportButton.onclick = function(e) {
|
||||
debugLog('Export button clicked');
|
||||
e.preventDefault();
|
||||
|
||||
// Get selected format with fallback
|
||||
const selectedRadio = document.querySelector('input[name="exportFormat"]:checked');
|
||||
const exportFormat = selectedRadio ? selectedRadio.value : 'csv';
|
||||
|
||||
debugLog('Selected format', exportFormat);
|
||||
exportAuditData(exportFormat);
|
||||
return false;
|
||||
};
|
||||
|
||||
// Set up pagination buttons
|
||||
document.getElementById('page-first').onclick = function() {
|
||||
debugLog('First page button clicked');
|
||||
if (currentPage > 0) {
|
||||
goToPage(0);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById('page-prev').onclick = function() {
|
||||
debugLog('Previous page button clicked');
|
||||
if (currentPage > 0) {
|
||||
goToPage(currentPage - 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById('page-next').onclick = function() {
|
||||
debugLog('Next page button clicked');
|
||||
if (currentPage < totalPages - 1) {
|
||||
goToPage(currentPage + 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById('page-last').onclick = function() {
|
||||
debugLog('Last page button clicked');
|
||||
if (totalPages > 0 && currentPage < totalPages - 1) {
|
||||
goToPage(totalPages - 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Set up tab change events
|
||||
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]');
|
||||
tabEls.forEach(tabEl => {
|
||||
tabEl.addEventListener('shown.bs.tab', function (event) {
|
||||
const targetId = event.target.getAttribute('data-bs-target');
|
||||
if (targetId === '#dashboard') {
|
||||
// Redraw charts when dashboard tab is shown
|
||||
if (typeChart) typeChart.update();
|
||||
if (userChart) userChart.update();
|
||||
if (timeChart) timeChart.update();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Load audit data from server
|
||||
function loadAuditData(targetPage, realPageSize) {
|
||||
const requestedPage = targetPage !== undefined ? targetPage : window.requestedPage || 0;
|
||||
realPageSize = realPageSize || pageSize;
|
||||
|
||||
debugLog('Loading audit data', {
|
||||
currentPage: currentPage,
|
||||
requestedPage: requestedPage,
|
||||
pageSize: pageSize,
|
||||
realPageSize: realPageSize
|
||||
});
|
||||
|
||||
showLoading('table-loading');
|
||||
|
||||
// Always request page 0 from server, but with increased page size if needed
|
||||
let url = `/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
||||
|
||||
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
|
||||
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
|
||||
if (startDateFilter) url += `&startDate=${startDateFilter}`;
|
||||
if (endDateFilter) url += `&endDate=${endDateFilter}`;
|
||||
|
||||
debugLog('Fetching URL', url);
|
||||
|
||||
// Update page indicator
|
||||
if (document.getElementById('page-indicator')) {
|
||||
document.getElementById('page-indicator').textContent = `Page ${requestedPage + 1} of ?`;
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
debugLog('Response received', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
debugLog('Data received', {
|
||||
totalPages: data.totalPages,
|
||||
serverPage: data.currentPage,
|
||||
totalElements: data.totalElements,
|
||||
contentSize: data.content.length
|
||||
});
|
||||
|
||||
// Calculate the correct slice of data to show for the requested page
|
||||
let displayContent = data.content;
|
||||
|
||||
// Render the correct slice of data
|
||||
renderTable(displayContent);
|
||||
|
||||
// Calculate total pages based on the actual total elements
|
||||
const calculatedTotalPages = Math.ceil(data.totalElements / realPageSize);
|
||||
totalPages = calculatedTotalPages;
|
||||
currentPage = requestedPage; // Use our tracked page, not server's
|
||||
|
||||
debugLog('Pagination state updated', {
|
||||
totalPages: totalPages,
|
||||
currentPage: currentPage
|
||||
});
|
||||
|
||||
// Update UI
|
||||
document.getElementById('currentPage').textContent = currentPage + 1;
|
||||
document.getElementById('totalPages').textContent = totalPages;
|
||||
document.getElementById('totalRecords').textContent = data.totalElements;
|
||||
if (document.getElementById('page-indicator')) {
|
||||
document.getElementById('page-indicator').textContent = `Page ${currentPage + 1} of ${totalPages}`;
|
||||
}
|
||||
|
||||
// Re-enable buttons with correct state
|
||||
document.getElementById('page-first').disabled = currentPage === 0;
|
||||
document.getElementById('page-prev').disabled = currentPage === 0;
|
||||
document.getElementById('page-next').disabled = currentPage >= totalPages - 1;
|
||||
document.getElementById('page-last').disabled = currentPage >= totalPages - 1;
|
||||
|
||||
hideLoading('table-loading');
|
||||
|
||||
// Restore original page size for next operations
|
||||
if (window.originalPageSize && realPageSize !== window.originalPageSize) {
|
||||
pageSize = window.originalPageSize;
|
||||
debugLog('Restored original page size', pageSize);
|
||||
}
|
||||
|
||||
// Store original page size for recovery
|
||||
window.originalPageSize = realPageSize;
|
||||
|
||||
// Clear busy flag
|
||||
window.paginationBusy = false;
|
||||
debugLog('Pagination completed successfully');
|
||||
})
|
||||
.catch(error => {
|
||||
debugLog('Error loading data', error.message);
|
||||
if (auditTableBody) {
|
||||
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">Error loading data: ${error.message}</td></tr>`;
|
||||
}
|
||||
hideLoading('table-loading');
|
||||
|
||||
// Re-enable buttons
|
||||
document.getElementById('page-first').disabled = false;
|
||||
document.getElementById('page-prev').disabled = false;
|
||||
document.getElementById('page-next').disabled = false;
|
||||
document.getElementById('page-last').disabled = false;
|
||||
|
||||
// Clear busy flag
|
||||
window.paginationBusy = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Load statistics for charts
|
||||
function loadStats(days) {
|
||||
showLoading('type-chart-loading');
|
||||
showLoading('user-chart-loading');
|
||||
showLoading('time-chart-loading');
|
||||
|
||||
fetch(`/audit/stats?days=${days}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('total-events').textContent = data.totalEvents;
|
||||
renderCharts(data);
|
||||
hideLoading('type-chart-loading');
|
||||
hideLoading('user-chart-loading');
|
||||
hideLoading('time-chart-loading');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading stats:', error);
|
||||
hideLoading('type-chart-loading');
|
||||
hideLoading('user-chart-loading');
|
||||
hideLoading('time-chart-loading');
|
||||
});
|
||||
}
|
||||
|
||||
// Export audit data
|
||||
function exportAuditData(format) {
|
||||
const type = exportTypeFilter.value.trim();
|
||||
const principal = exportPrincipalFilter.value.trim();
|
||||
const startDate = exportStartDateFilter.value;
|
||||
const endDate = exportEndDateFilter.value;
|
||||
|
||||
let url = format === 'json' ? '/audit/export/json?' : '/audit/export?';
|
||||
|
||||
if (type) url += `&type=${encodeURIComponent(type)}`;
|
||||
if (principal) url += `&principal=${encodeURIComponent(principal)}`;
|
||||
if (startDate) url += `&startDate=${startDate}`;
|
||||
if (endDate) url += `&endDate=${endDate}`;
|
||||
|
||||
// Trigger download
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
// Render table with audit data
|
||||
function renderTable(events) {
|
||||
debugLog('renderTable called with', events ? events.length : 0, 'events');
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
debugLog('No events to render');
|
||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">No audit events found matching the current filters</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
debugLog('Clearing table body');
|
||||
auditTableBody.innerHTML = '';
|
||||
|
||||
debugLog('Processing events for table');
|
||||
events.forEach((event, index) => {
|
||||
try {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${event.id || 'N/A'}</td>
|
||||
<td>${formatDate(event.timestamp)}</td>
|
||||
<td>${escapeHtml(event.principal || 'N/A')}</td>
|
||||
<td>${escapeHtml(event.type || 'N/A')}</td>
|
||||
<td><button class="btn btn-sm btn-outline-primary view-details">View Details</button></td>
|
||||
`;
|
||||
|
||||
// Store event data for modal
|
||||
row.dataset.event = JSON.stringify(event);
|
||||
|
||||
// Add click handler for details button
|
||||
const detailsButton = row.querySelector('.view-details');
|
||||
if (detailsButton) {
|
||||
detailsButton.addEventListener('click', function() {
|
||||
showEventDetails(event);
|
||||
});
|
||||
}
|
||||
|
||||
auditTableBody.appendChild(row);
|
||||
} catch (rowError) {
|
||||
debugLog('Error rendering row ' + index, rowError.message);
|
||||
}
|
||||
});
|
||||
|
||||
debugLog('Table rendering complete');
|
||||
} catch (e) {
|
||||
debugLog('Error in renderTable', e.message);
|
||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Error rendering table: ' + e.message + '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// Show event details in modal
|
||||
function showEventDetails(event) {
|
||||
modalId.textContent = event.id;
|
||||
modalPrincipal.textContent = event.principal;
|
||||
modalType.textContent = event.type;
|
||||
modalTimestamp.textContent = formatDate(event.timestamp);
|
||||
|
||||
// Format JSON data
|
||||
try {
|
||||
const dataObj = JSON.parse(event.data);
|
||||
modalData.textContent = JSON.stringify(dataObj, null, 2);
|
||||
} catch (e) {
|
||||
modalData.textContent = event.data;
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
const modal = new bootstrap.Modal(eventDetailsModal);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// No need for a dynamic pagination renderer anymore as we're using static buttons
|
||||
|
||||
// Direct pagination approach - server seems to be hard-limited to returning 20 items
|
||||
function goToPage(page) {
|
||||
debugLog('goToPage called with page', page);
|
||||
|
||||
// Basic validation - totalPages may not be initialized on first load
|
||||
if (page < 0) {
|
||||
debugLog('Invalid page', page);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip validation against totalPages on first load
|
||||
if (totalPages > 0 && page >= totalPages) {
|
||||
debugLog('Page exceeds total pages', page);
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple guard flag
|
||||
if (window.paginationBusy) {
|
||||
debugLog('Pagination busy, ignoring request');
|
||||
return;
|
||||
}
|
||||
window.paginationBusy = true;
|
||||
|
||||
try {
|
||||
debugLog('Setting page to', page);
|
||||
|
||||
// Store the requested page for later
|
||||
window.requestedPage = page;
|
||||
currentPage = page;
|
||||
|
||||
// Update UI immediately for user feedback
|
||||
document.getElementById('currentPage').textContent = page + 1;
|
||||
|
||||
// Load data with this page
|
||||
loadAuditData(page, pageSize);
|
||||
} catch (e) {
|
||||
debugLog('Error in pagination', e.message);
|
||||
window.paginationBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Render charts
|
||||
function renderCharts(data) {
|
||||
// Prepare data for charts
|
||||
const typeLabels = Object.keys(data.eventsByType);
|
||||
const typeValues = Object.values(data.eventsByType);
|
||||
|
||||
const userLabels = Object.keys(data.eventsByPrincipal);
|
||||
const userValues = Object.values(data.eventsByPrincipal);
|
||||
|
||||
// Sort days for time chart
|
||||
const timeLabels = Object.keys(data.eventsByDay).sort();
|
||||
const timeValues = timeLabels.map(day => data.eventsByDay[day] || 0);
|
||||
|
||||
// Type chart
|
||||
if (typeChart) {
|
||||
typeChart.destroy();
|
||||
}
|
||||
|
||||
const typeCtx = document.getElementById('typeChart').getContext('2d');
|
||||
typeChart = new Chart(typeCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: typeLabels,
|
||||
datasets: [{
|
||||
label: 'Events by Type',
|
||||
data: typeValues,
|
||||
backgroundColor: getChartColors(typeLabels.length),
|
||||
borderColor: getChartColors(typeLabels.length, 1), // Full opacity for borders
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// User chart
|
||||
if (userChart) {
|
||||
userChart.destroy();
|
||||
}
|
||||
|
||||
const userCtx = document.getElementById('userChart').getContext('2d');
|
||||
userChart = new Chart(userCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: userLabels,
|
||||
datasets: [{
|
||||
label: 'Events by User',
|
||||
data: userValues,
|
||||
backgroundColor: getChartColors(userLabels.length),
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false
|
||||
}
|
||||
});
|
||||
|
||||
// Time chart
|
||||
if (timeChart) {
|
||||
timeChart.destroy();
|
||||
}
|
||||
|
||||
const timeCtx = document.getElementById('timeChart').getContext('2d');
|
||||
timeChart = new Chart(timeCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: timeLabels,
|
||||
datasets: [{
|
||||
label: 'Events Over Time',
|
||||
data: timeValues,
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
tension: 0.1,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function formatDate(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
return text
|
||||
.toString()
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function showLoading(id) {
|
||||
const loading = document.getElementById(id);
|
||||
if (loading) loading.style.display = 'flex';
|
||||
}
|
||||
|
||||
function hideLoading(id) {
|
||||
const loading = document.getElementById(id);
|
||||
if (loading) loading.style.display = 'none';
|
||||
}
|
||||
|
||||
// Load event types from the server for filter dropdowns
|
||||
function loadEventTypes() {
|
||||
fetch('/audit/types')
|
||||
.then(response => response.json())
|
||||
.then(types => {
|
||||
if (!types || types.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate the type filter dropdowns
|
||||
const typeFilter = document.getElementById('typeFilter');
|
||||
const exportTypeFilter = document.getElementById('exportTypeFilter');
|
||||
|
||||
// Clear existing options except the first one (All event types)
|
||||
while (typeFilter.options.length > 1) {
|
||||
typeFilter.remove(1);
|
||||
}
|
||||
|
||||
while (exportTypeFilter.options.length > 1) {
|
||||
exportTypeFilter.remove(1);
|
||||
}
|
||||
|
||||
// Add new options
|
||||
types.forEach(type => {
|
||||
// Main filter dropdown
|
||||
const option = document.createElement('option');
|
||||
option.value = type;
|
||||
option.textContent = type;
|
||||
typeFilter.appendChild(option);
|
||||
|
||||
// Export filter dropdown
|
||||
const exportOption = document.createElement('option');
|
||||
exportOption.value = type;
|
||||
exportOption.textContent = type;
|
||||
exportTypeFilter.appendChild(exportOption);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading event types:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to generate a palette of colors for charts
|
||||
function getChartColors(count, opacity = 0.6) {
|
||||
// Base colors - a larger palette than the default
|
||||
const colors = [
|
||||
[54, 162, 235], // blue
|
||||
[255, 99, 132], // red
|
||||
[75, 192, 192], // teal
|
||||
[255, 206, 86], // yellow
|
||||
[153, 102, 255], // purple
|
||||
[255, 159, 64], // orange
|
||||
[46, 204, 113], // green
|
||||
[231, 76, 60], // dark red
|
||||
[52, 152, 219], // light blue
|
||||
[155, 89, 182], // violet
|
||||
[241, 196, 15], // dark yellow
|
||||
[26, 188, 156], // turquoise
|
||||
[230, 126, 34], // dark orange
|
||||
[149, 165, 166], // light gray
|
||||
[243, 156, 18], // amber
|
||||
[39, 174, 96], // emerald
|
||||
[211, 84, 0], // dark orange red
|
||||
[22, 160, 133], // green sea
|
||||
[41, 128, 185], // belize hole
|
||||
[142, 68, 173] // wisteria
|
||||
];
|
||||
|
||||
const result = [];
|
||||
|
||||
// Always use the same format regardless of color source
|
||||
if (count > colors.length) {
|
||||
// Generate colors algorithmically for large sets
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Generate a color based on position in the hue circle (0-360)
|
||||
const hue = (i * 360 / count) % 360;
|
||||
const sat = 70 + Math.random() * 10; // 70-80%
|
||||
const light = 50 + Math.random() * 10; // 50-60%
|
||||
|
||||
result.push(`hsla(${hue}, ${sat}%, ${light}%, ${opacity})`);
|
||||
}
|
||||
} else {
|
||||
// Use colors from our palette but also return in hsla format for consistency
|
||||
for (let i = 0; i < count; i++) {
|
||||
const color = colors[i % colors.length];
|
||||
result.push(`rgba(${color[0]}, ${color[1]}, ${color[2]}, ${opacity})`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
@ -6,99 +6,13 @@
|
||||
<!-- Include Chart.js for visualizations -->
|
||||
<script th:src="@{/js/thirdParty/chart.umd.min.js}"></script>
|
||||
|
||||
<style>
|
||||
.dashboard-card {
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
width: 100%;
|
||||
}
|
||||
.filter-card {
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
}
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.level-indicator {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 15px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
.level-0 {
|
||||
background-color: #dc3545; /* Red */
|
||||
}
|
||||
.level-1 {
|
||||
background-color: #fd7e14; /* Orange */
|
||||
}
|
||||
.level-2 {
|
||||
background-color: #28a745; /* Green */
|
||||
}
|
||||
.level-3 {
|
||||
background-color: #17a2b8; /* Teal */
|
||||
}
|
||||
/* Custom data table styling */
|
||||
.audit-table {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.audit-table th {
|
||||
background-color: #f8f9fa;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.table-responsive {
|
||||
max-height: 600px;
|
||||
}
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.json-viewer {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
<!-- Include custom CSS -->
|
||||
<link rel="stylesheet" th:href="@{/css/audit-dashboard.css}" />
|
||||
</head>
|
||||
<body>
|
||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<!-- DO NOT REMOVE <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block> -->
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<h1 class="mb-4">Audit Dashboard</h1>
|
||||
@ -237,7 +151,10 @@
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="typeFilter" class="form-label">Event Type</label>
|
||||
<input type="text" class="form-control" id="typeFilter" placeholder="Filter by type">
|
||||
<select class="form-select" id="typeFilter">
|
||||
<option value="">All event types</option>
|
||||
<!-- Will be populated from API -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@ -301,11 +218,16 @@
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span>entries</span>
|
||||
<span class="mx-3">Page <span id="currentPage">1</span> of <span id="totalPages">1</span> (Total records: <span id="totalRecords">0</span>)</span>
|
||||
</div>
|
||||
<nav aria-label="Audit events pagination">
|
||||
<ul class="pagination" id="pagination">
|
||||
<!-- Pagination will be populated by JavaScript -->
|
||||
</ul>
|
||||
<div class="btn-group" role="group" aria-label="Pagination">
|
||||
<button type="button" class="btn btn-outline-primary" id="page-first">«</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="page-prev">‹</button>
|
||||
<span class="btn btn-outline-secondary disabled" id="page-indicator">Page 1 of 1</span>
|
||||
<button type="button" class="btn btn-outline-primary" id="page-next">›</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="page-last">»</button>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@ -364,7 +286,10 @@
|
||||
<div class="col-md-3">
|
||||
<div class="mb-3">
|
||||
<label for="exportTypeFilter" class="form-label">Event Type</label>
|
||||
<input type="text" class="form-control" id="exportTypeFilter" placeholder="Filter by type">
|
||||
<select class="form-select" id="exportTypeFilter">
|
||||
<option value="">All event types</option>
|
||||
<!-- Will be populated from API -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
@ -389,15 +314,13 @@
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<h5>Export Format</h5>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="exportFormat" id="formatCSV" value="csv" checked>
|
||||
<label class="form-check-label" for="formatCSV">
|
||||
<div>
|
||||
<label class="btn btn-outline-primary" style="margin-right: 10px;">
|
||||
<input type="radio" name="exportFormat" id="formatCSV" value="csv" checked style="margin-right: 5px;">
|
||||
CSV (Comma Separated Values)
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="exportFormat" id="formatJSON" value="json">
|
||||
<label class="form-check-label" for="formatJSON">
|
||||
<label class="btn btn-outline-primary">
|
||||
<input type="radio" name="exportFormat" id="formatJSON" value="json" style="margin-right: 5px;">
|
||||
JSON (JavaScript Object Notation)
|
||||
</label>
|
||||
</div>
|
||||
@ -406,6 +329,9 @@
|
||||
<button id="exportButton" class="btn btn-primary mt-4">
|
||||
<i class="bi bi-download"></i> Export Data
|
||||
</button>
|
||||
<button id="resetExportFilters" class="btn btn-secondary mt-4 ms-2">
|
||||
Reset Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -524,7 +450,14 @@
|
||||
<script th:src="@{/js/thirdParty/jquery.min.js}"></script>
|
||||
<script th:src="@{/js/thirdParty/bootstrap.min.js}"></script>
|
||||
|
||||
<!-- Debug console for development purposes -->
|
||||
<div id="debug-console"></div>
|
||||
|
||||
<!-- Load custom JavaScript -->
|
||||
<script th:src="@{/js/audit/dashboard.js}"></script>
|
||||
|
||||
<script>
|
||||
// DEPRECATED - KEPT FOR REFERENCE - USE dashboard.js INSTEAD
|
||||
// Initialize variables
|
||||
let currentPage = 0;
|
||||
let pageSize = 20;
|
||||
@ -539,16 +472,17 @@
|
||||
let userChart;
|
||||
let timeChart;
|
||||
|
||||
// DOM elements
|
||||
const auditTableBody = document.getElementById('auditTableBody');
|
||||
const pagination = document.getElementById('pagination');
|
||||
const pageSizeSelect = document.getElementById('pageSizeSelect');
|
||||
const typeFilterInput = document.getElementById('typeFilter');
|
||||
const principalFilterInput = document.getElementById('principalFilter');
|
||||
const startDateFilterInput = document.getElementById('startDateFilter');
|
||||
const endDateFilterInput = document.getElementById('endDateFilter');
|
||||
const applyFiltersButton = document.getElementById('applyFilters');
|
||||
const resetFiltersButton = document.getElementById('resetFilters');
|
||||
// DOM elements - will properly initialize these during page load
|
||||
let auditTableBody;
|
||||
let pagination;
|
||||
let pageSizeSelect;
|
||||
let typeFilterInput;
|
||||
let exportTypeFilterInput;
|
||||
let principalFilterInput;
|
||||
let startDateFilterInput;
|
||||
let endDateFilterInput;
|
||||
let applyFiltersButton;
|
||||
let resetFiltersButton;
|
||||
|
||||
// Modal elements
|
||||
const eventDetailsModal = document.getElementById('eventDetailsModal');
|
||||
@ -564,18 +498,110 @@
|
||||
const exportStartDateFilter = document.getElementById('exportStartDateFilter');
|
||||
const exportEndDateFilter = document.getElementById('exportEndDateFilter');
|
||||
const exportButton = document.getElementById('exportButton');
|
||||
const formatRadios = document.querySelectorAll('input[name="exportFormat"]');
|
||||
|
||||
// Debug logger function
|
||||
let debugEnabled = true;
|
||||
function debugLog(message, data) {
|
||||
if (!debugEnabled) return;
|
||||
|
||||
const console = document.getElementById('debug-console');
|
||||
if (console) {
|
||||
if (console.style.display === 'none' || !console.style.display) {
|
||||
console.style.display = 'block';
|
||||
}
|
||||
|
||||
const time = new Date().toLocaleTimeString();
|
||||
let logMessage = `[${time}] ${message}`;
|
||||
|
||||
if (data !== undefined) {
|
||||
if (typeof data === 'object') {
|
||||
try {
|
||||
logMessage += ': ' + JSON.stringify(data);
|
||||
} catch (e) {
|
||||
logMessage += ': ' + data;
|
||||
}
|
||||
} else {
|
||||
logMessage += ': ' + data;
|
||||
}
|
||||
}
|
||||
|
||||
const logLine = document.createElement('div');
|
||||
logLine.textContent = logMessage;
|
||||
console.appendChild(logLine);
|
||||
console.scrollTop = console.scrollHeight;
|
||||
|
||||
// Keep only last 100 lines
|
||||
while (console.childNodes.length > 100) {
|
||||
console.removeChild(console.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Also log to browser console
|
||||
if (data !== undefined) {
|
||||
window.console.log(message, data);
|
||||
} else {
|
||||
window.console.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Add keyboard shortcut to toggle debug console
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'F12' && e.ctrlKey) {
|
||||
const console = document.getElementById('debug-console');
|
||||
if (console) {
|
||||
console.style.display = console.style.display === 'none' || !console.style.display ? 'block' : 'none';
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load initial data
|
||||
loadAuditData();
|
||||
debugLog('Page initialized');
|
||||
|
||||
// Initialize DOM references
|
||||
auditTableBody = document.getElementById('auditTableBody');
|
||||
pageSizeSelect = document.getElementById('pageSizeSelect');
|
||||
typeFilterInput = document.getElementById('typeFilter');
|
||||
exportTypeFilterInput = document.getElementById('exportTypeFilter');
|
||||
principalFilterInput = document.getElementById('principalFilter');
|
||||
startDateFilterInput = document.getElementById('startDateFilter');
|
||||
endDateFilterInput = document.getElementById('endDateFilter');
|
||||
applyFiltersButton = document.getElementById('applyFilters');
|
||||
resetFiltersButton = document.getElementById('resetFilters');
|
||||
|
||||
// Debug log DOM elements
|
||||
debugLog('DOM elements initialized', {
|
||||
auditTableBody: !!auditTableBody,
|
||||
pagination: !!pagination,
|
||||
pageSizeSelect: !!pageSizeSelect
|
||||
});
|
||||
|
||||
// Load event types for dropdowns
|
||||
loadEventTypes();
|
||||
|
||||
// Show a loading message immediately
|
||||
if (auditTableBody) {
|
||||
auditTableBody.innerHTML =
|
||||
'<tr><td colspan="5" class="text-center"><div class="spinner-border spinner-border-sm" role="status"></div> Loading audit data...</td></tr>';
|
||||
} else {
|
||||
debugLog('ERROR: auditTableBody element not found!');
|
||||
}
|
||||
|
||||
// Make a direct API call first to avoid validation issues
|
||||
loadAuditData(0, pageSize);
|
||||
|
||||
// Load statistics for dashboard
|
||||
loadStats(7);
|
||||
|
||||
// Set up event listeners
|
||||
pageSizeSelect.addEventListener('change', function() {
|
||||
pageSize = parseInt(this.value);
|
||||
window.originalPageSize = pageSize;
|
||||
currentPage = 0;
|
||||
loadAuditData();
|
||||
window.requestedPage = 0;
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
applyFiltersButton.addEventListener('click', function() {
|
||||
@ -584,63 +610,232 @@
|
||||
startDateFilter = startDateFilterInput.value;
|
||||
endDateFilter = endDateFilterInput.value;
|
||||
currentPage = 0;
|
||||
loadAuditData();
|
||||
window.requestedPage = 0;
|
||||
debugLog('Applying filters and resetting to page 0');
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
resetFiltersButton.addEventListener('click', function() {
|
||||
// Reset input fields
|
||||
typeFilterInput.value = '';
|
||||
principalFilterInput.value = '';
|
||||
startDateFilterInput.value = '';
|
||||
endDateFilterInput.value = '';
|
||||
|
||||
// Reset filter variables
|
||||
typeFilter = '';
|
||||
principalFilter = '';
|
||||
startDateFilter = '';
|
||||
endDateFilter = '';
|
||||
|
||||
// Reset page
|
||||
currentPage = 0;
|
||||
loadAuditData();
|
||||
window.requestedPage = 0;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('currentPage').textContent = '1';
|
||||
|
||||
debugLog('Resetting filters and going to page 0');
|
||||
|
||||
// Load data with reset filters
|
||||
loadAuditData(0, pageSize);
|
||||
});
|
||||
|
||||
exportButton.addEventListener('click', function() {
|
||||
const exportFormat = document.querySelector('input[name="exportFormat"]:checked').value;
|
||||
exportAuditData(exportFormat);
|
||||
// Reset export filters button
|
||||
document.getElementById('resetExportFilters').addEventListener('click', function() {
|
||||
exportTypeFilter.value = '';
|
||||
exportPrincipalFilter.value = '';
|
||||
exportStartDateFilter.value = '';
|
||||
exportEndDateFilter.value = '';
|
||||
});
|
||||
|
||||
// Make radio buttons behave like toggle buttons
|
||||
const radioLabels = document.querySelectorAll('label.btn-outline-primary');
|
||||
radioLabels.forEach(label => {
|
||||
const radio = label.querySelector('input[type="radio"]');
|
||||
|
||||
if (radio) {
|
||||
// Highlight the checked radio button's label
|
||||
if (radio.checked) {
|
||||
label.classList.add('active');
|
||||
}
|
||||
|
||||
// Handle clicking on the label
|
||||
label.addEventListener('click', function() {
|
||||
// Remove active class from all labels
|
||||
radioLabels.forEach(l => l.classList.remove('active'));
|
||||
|
||||
// Add active class to this label
|
||||
this.classList.add('active');
|
||||
|
||||
// Check this radio button
|
||||
radio.checked = true;
|
||||
|
||||
debugLog('Radio format selected', radio.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle export button with debug
|
||||
exportButton.onclick = function(e) {
|
||||
debugLog('Export button clicked');
|
||||
e.preventDefault();
|
||||
|
||||
// Get selected format with fallback
|
||||
const selectedRadio = document.querySelector('input[name="exportFormat"]:checked');
|
||||
const exportFormat = selectedRadio ? selectedRadio.value : 'csv';
|
||||
|
||||
debugLog('Selected format', exportFormat);
|
||||
exportAuditData(exportFormat);
|
||||
return false;
|
||||
};
|
||||
|
||||
// Set up pagination buttons
|
||||
document.getElementById('page-first').onclick = function() {
|
||||
debugLog('First page button clicked');
|
||||
if (currentPage > 0) {
|
||||
goToPage(0);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById('page-prev').onclick = function() {
|
||||
debugLog('Previous page button clicked');
|
||||
if (currentPage > 0) {
|
||||
goToPage(currentPage - 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById('page-next').onclick = function() {
|
||||
debugLog('Next page button clicked');
|
||||
if (currentPage < totalPages - 1) {
|
||||
goToPage(currentPage + 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
document.getElementById('page-last').onclick = function() {
|
||||
debugLog('Last page button clicked');
|
||||
if (totalPages > 0 && currentPage < totalPages - 1) {
|
||||
goToPage(totalPages - 1);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Set up tab change events
|
||||
const tabEl = document.querySelector('button[data-bs-toggle="tab"]');
|
||||
tabEl.addEventListener('shown.bs.tab', function (event) {
|
||||
const targetId = event.target.getAttribute('data-bs-target');
|
||||
if (targetId === '#dashboard') {
|
||||
// Redraw charts when dashboard tab is shown
|
||||
if (typeChart) typeChart.update();
|
||||
if (userChart) userChart.update();
|
||||
if (timeChart) timeChart.update();
|
||||
}
|
||||
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]');
|
||||
tabEls.forEach(tabEl => {
|
||||
tabEl.addEventListener('shown.bs.tab', function (event) {
|
||||
const targetId = event.target.getAttribute('data-bs-target');
|
||||
if (targetId === '#dashboard') {
|
||||
// Redraw charts when dashboard tab is shown
|
||||
if (typeChart) typeChart.update();
|
||||
if (userChart) userChart.update();
|
||||
if (timeChart) timeChart.update();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Load audit data from server
|
||||
function loadAuditData() {
|
||||
// Load audit data from server - completely rewritten with client-side pagination
|
||||
function loadAuditData(targetPage, originalPageSize) {
|
||||
const requestedPage = targetPage !== undefined ? targetPage : window.requestedPage || 0;
|
||||
const realPageSize = originalPageSize || pageSize;
|
||||
|
||||
debugLog('Loading audit data', {
|
||||
currentPage: currentPage,
|
||||
requestedPage: requestedPage,
|
||||
pageSize: pageSize,
|
||||
realPageSize: realPageSize
|
||||
});
|
||||
|
||||
showLoading('table-loading');
|
||||
|
||||
let url = `/audit/data?page=${currentPage}&size=${pageSize}`;
|
||||
// Always request page 0 from server, but with increased page size if needed
|
||||
let url = `/audit/data?page=${requestedPage}&size=${realPageSize}`;
|
||||
|
||||
if (typeFilter) url += `&type=${encodeURIComponent(typeFilter)}`;
|
||||
if (principalFilter) url += `&principal=${encodeURIComponent(principalFilter)}`;
|
||||
if (startDateFilter) url += `&startDate=${startDateFilter}`;
|
||||
if (endDateFilter) url += `&endDate=${endDateFilter}`;
|
||||
|
||||
debugLog('Fetching URL', url);
|
||||
|
||||
// Update page indicator immediately for better UX
|
||||
document.getElementById('page-indicator').textContent = `Page ${requestedPage + 1} of ?`;
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
debugLog('Response received', response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
renderTable(data.content);
|
||||
renderPagination(data.totalPages, data.currentPage);
|
||||
totalPages = data.totalPages;
|
||||
debugLog('Data received', {
|
||||
totalPages: data.totalPages,
|
||||
serverPage: data.currentPage,
|
||||
totalElements: data.totalElements,
|
||||
contentSize: data.content.length
|
||||
});
|
||||
|
||||
// Calculate the correct slice of data to show for the requested page
|
||||
let displayContent = data.content;
|
||||
|
||||
|
||||
|
||||
// Render the correct slice of data
|
||||
renderTable(displayContent);
|
||||
|
||||
// Calculate total pages based on the actual total elements
|
||||
const calculatedTotalPages = Math.ceil(data.totalElements / realPageSize);
|
||||
totalPages = calculatedTotalPages;
|
||||
currentPage = requestedPage; // Use our tracked page, not server's
|
||||
|
||||
debugLog('Pagination state updated', {
|
||||
totalPages: totalPages,
|
||||
currentPage: currentPage
|
||||
});
|
||||
|
||||
// Update UI
|
||||
document.getElementById('currentPage').textContent = currentPage + 1;
|
||||
document.getElementById('totalPages').textContent = totalPages;
|
||||
document.getElementById('totalRecords').textContent = data.totalElements;
|
||||
document.getElementById('page-indicator').textContent = `Page ${currentPage + 1} of ${totalPages}`;
|
||||
|
||||
// Re-enable buttons with correct state
|
||||
document.getElementById('page-first').disabled = currentPage === 0;
|
||||
document.getElementById('page-prev').disabled = currentPage === 0;
|
||||
document.getElementById('page-next').disabled = currentPage >= totalPages - 1;
|
||||
document.getElementById('page-last').disabled = currentPage >= totalPages - 1;
|
||||
|
||||
hideLoading('table-loading');
|
||||
|
||||
// Restore original page size for next operations
|
||||
if (originalPageSize) {
|
||||
pageSize = originalPageSize;
|
||||
debugLog('Restored original page size', pageSize);
|
||||
}
|
||||
|
||||
// Store original page size for recovery
|
||||
window.originalPageSize = realPageSize;
|
||||
|
||||
// Clear busy flag
|
||||
window.paginationBusy = false;
|
||||
debugLog('Pagination completed successfully');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading audit data:', error);
|
||||
debugLog('Error loading data', error.message);
|
||||
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">Error loading data: ${error.message}</td></tr>`;
|
||||
hideLoading('table-loading');
|
||||
|
||||
// Re-enable buttons
|
||||
document.getElementById('page-first').disabled = false;
|
||||
document.getElementById('page-prev').disabled = false;
|
||||
document.getElementById('page-next').disabled = false;
|
||||
document.getElementById('page-last').disabled = false;
|
||||
|
||||
// Clear busy flag
|
||||
window.paginationBusy = false;
|
||||
});
|
||||
}
|
||||
|
||||
@ -687,33 +882,52 @@
|
||||
|
||||
// Render table with audit data
|
||||
function renderTable(events) {
|
||||
debugLog('renderTable called with', events ? events.length : 0, 'events');
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">No audit events found</td></tr>';
|
||||
debugLog('No events to render');
|
||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">No audit events found matching the current filters</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
auditTableBody.innerHTML = '';
|
||||
|
||||
events.forEach(event => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${event.id}</td>
|
||||
<td>${formatDate(event.timestamp)}</td>
|
||||
<td>${escapeHtml(event.principal)}</td>
|
||||
<td>${escapeHtml(event.type)}</td>
|
||||
<td><button class="btn btn-sm btn-outline-primary view-details">View Details</button></td>
|
||||
`;
|
||||
try {
|
||||
debugLog('Clearing table body');
|
||||
auditTableBody.innerHTML = '';
|
||||
|
||||
// Store event data for modal
|
||||
row.dataset.event = JSON.stringify(event);
|
||||
|
||||
// Add click handler for details button
|
||||
row.querySelector('.view-details').addEventListener('click', function() {
|
||||
showEventDetails(event);
|
||||
debugLog('Processing events for table');
|
||||
events.forEach((event, index) => {
|
||||
try {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${event.id || 'N/A'}</td>
|
||||
<td>${formatDate(event.timestamp)}</td>
|
||||
<td>${escapeHtml(event.principal || 'N/A')}</td>
|
||||
<td>${escapeHtml(event.type || 'N/A')}</td>
|
||||
<td><button class="btn btn-sm btn-outline-primary view-details">View Details</button></td>
|
||||
`;
|
||||
|
||||
// Store event data for modal
|
||||
row.dataset.event = JSON.stringify(event);
|
||||
|
||||
// Add click handler for details button
|
||||
const detailsButton = row.querySelector('.view-details');
|
||||
if (detailsButton) {
|
||||
detailsButton.addEventListener('click', function() {
|
||||
showEventDetails(event);
|
||||
});
|
||||
}
|
||||
|
||||
auditTableBody.appendChild(row);
|
||||
} catch (rowError) {
|
||||
debugLog('Error rendering row ' + index, rowError.message);
|
||||
}
|
||||
});
|
||||
|
||||
auditTableBody.appendChild(row);
|
||||
});
|
||||
debugLog('Table rendering complete');
|
||||
} catch (e) {
|
||||
debugLog('Error in renderTable', e.message);
|
||||
auditTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Error rendering table: ' + e.message + '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// Show event details in modal
|
||||
@ -736,76 +950,112 @@
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Render pagination controls
|
||||
function renderPagination(totalPages, currentPage) {
|
||||
pagination.innerHTML = '';
|
||||
// No need for a dynamic pagination renderer anymore as we're using static buttons
|
||||
|
||||
// Direct pagination approach - server seems to be hard-limited to returning 20 items
|
||||
function goToPage(page) {
|
||||
debugLog('goToPage called with page', page);
|
||||
|
||||
// Previous button
|
||||
const prevLi = document.createElement('li');
|
||||
prevLi.classList.add('page-item');
|
||||
if (currentPage === 0) prevLi.classList.add('disabled');
|
||||
|
||||
const prevLink = document.createElement('a');
|
||||
prevLink.classList.add('page-link');
|
||||
prevLink.href = '#';
|
||||
prevLink.textContent = 'Previous';
|
||||
prevLink.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (currentPage > 0) {
|
||||
goToPage(currentPage - 1);
|
||||
}
|
||||
});
|
||||
|
||||
prevLi.appendChild(prevLink);
|
||||
pagination.appendChild(prevLi);
|
||||
|
||||
// Page numbers
|
||||
const maxPages = 5; // Max number of page links to show
|
||||
const startPage = Math.max(0, currentPage - Math.floor(maxPages / 2));
|
||||
const endPage = Math.min(totalPages - 1, startPage + maxPages - 1);
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
const pageLi = document.createElement('li');
|
||||
pageLi.classList.add('page-item');
|
||||
if (i === currentPage) pageLi.classList.add('active');
|
||||
|
||||
const pageLink = document.createElement('a');
|
||||
pageLink.classList.add('page-link');
|
||||
pageLink.href = '#';
|
||||
pageLink.textContent = i + 1;
|
||||
pageLink.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
goToPage(i);
|
||||
});
|
||||
|
||||
pageLi.appendChild(pageLink);
|
||||
pagination.appendChild(pageLi);
|
||||
// Basic validation - totalPages may not be initialized on first load
|
||||
if (page < 0) {
|
||||
debugLog('Invalid page', page);
|
||||
return;
|
||||
}
|
||||
|
||||
// Next button
|
||||
const nextLi = document.createElement('li');
|
||||
nextLi.classList.add('page-item');
|
||||
if (currentPage >= totalPages - 1) nextLi.classList.add('disabled');
|
||||
// Skip validation against totalPages on first load
|
||||
if (totalPages > 0 && page >= totalPages) {
|
||||
debugLog('Page exceeds total pages', page);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextLink = document.createElement('a');
|
||||
nextLink.classList.add('page-link');
|
||||
nextLink.href = '#';
|
||||
nextLink.textContent = 'Next';
|
||||
nextLink.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
if (currentPage < totalPages - 1) {
|
||||
goToPage(currentPage + 1);
|
||||
}
|
||||
});
|
||||
// Simple guard flag
|
||||
if (window.paginationBusy) {
|
||||
debugLog('Pagination busy, ignoring request');
|
||||
return;
|
||||
}
|
||||
window.paginationBusy = true;
|
||||
|
||||
nextLi.appendChild(nextLink);
|
||||
pagination.appendChild(nextLi);
|
||||
}
|
||||
|
||||
// Navigate to specific page
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
loadAuditData();
|
||||
try {
|
||||
debugLog('Setting page to', page);
|
||||
|
||||
// Store the requested page for later
|
||||
window.requestedPage = page;
|
||||
currentPage = page;
|
||||
|
||||
// Update UI immediately for user feedback
|
||||
document.getElementById('currentPage').textContent = page + 1;
|
||||
|
||||
// Show loading state
|
||||
document.getElementById('auditTableBody').innerHTML =
|
||||
'<tr><td colspan="5" class="text-center">Loading page ' + (page + 1) + '...</td></tr>';
|
||||
|
||||
// Disable pagination buttons during load
|
||||
document.getElementById('page-first').disabled = true;
|
||||
document.getElementById('page-prev').disabled = true;
|
||||
document.getElementById('page-next').disabled = true;
|
||||
document.getElementById('page-last').disabled = true;
|
||||
|
||||
// Request the specific page directly
|
||||
debugLog('Directly requesting server page', page);
|
||||
|
||||
// Force a new API call with the requested page
|
||||
let apiUrl = `/audit/data?page=${page}&size=${pageSize}`;
|
||||
|
||||
if (typeFilter) apiUrl += `&type=${encodeURIComponent(typeFilter)}`;
|
||||
if (principalFilter) apiUrl += `&principal=${encodeURIComponent(principalFilter)}`;
|
||||
if (startDateFilter) apiUrl += `&startDate=${startDateFilter}`;
|
||||
if (endDateFilter) apiUrl += `&endDate=${endDateFilter}`;
|
||||
|
||||
debugLog('Making direct API call to', apiUrl);
|
||||
|
||||
// Directly make the fetch here instead of using loadAuditData
|
||||
fetch(apiUrl)
|
||||
.then(response => {
|
||||
debugLog('Response received for page ' + page, response.status);
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
debugLog('Data received for page ' + page, {
|
||||
totalPages: data.totalPages,
|
||||
serverPage: data.currentPage,
|
||||
totalElements: data.totalElements,
|
||||
contentSize: data.content.length
|
||||
});
|
||||
|
||||
// Render the data directly
|
||||
renderTable(data.content);
|
||||
|
||||
// Update pagination state
|
||||
totalPages = data.totalPages;
|
||||
|
||||
// Update UI
|
||||
document.getElementById('currentPage').textContent = page + 1;
|
||||
document.getElementById('totalPages').textContent = totalPages;
|
||||
document.getElementById('totalRecords').textContent = data.totalElements;
|
||||
document.getElementById('page-indicator').textContent = `Page ${page + 1} of ${totalPages}`;
|
||||
|
||||
// Re-enable buttons with correct state
|
||||
document.getElementById('page-first').disabled = page === 0;
|
||||
document.getElementById('page-prev').disabled = page === 0;
|
||||
document.getElementById('page-next').disabled = page >= totalPages - 1;
|
||||
document.getElementById('page-last').disabled = page >= totalPages - 1;
|
||||
|
||||
hideLoading('table-loading');
|
||||
|
||||
// Clear busy flag
|
||||
window.paginationBusy = false;
|
||||
debugLog('Direct pagination completed successfully');
|
||||
})
|
||||
.catch(error => {
|
||||
debugLog('Error in direct pagination', error.message);
|
||||
auditTableBody.innerHTML = `<tr><td colspan="5" class="text-center">Error loading page ${page + 1}: ${error.message}</td></tr>`;
|
||||
hideLoading('table-loading');
|
||||
window.paginationBusy = false;
|
||||
});
|
||||
} catch (e) {
|
||||
debugLog('Error in pagination', e.message);
|
||||
window.paginationBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Render charts
|
||||
@ -931,6 +1181,48 @@
|
||||
if (loading) loading.style.display = 'none';
|
||||
}
|
||||
|
||||
// Load event types from the server for filter dropdowns
|
||||
function loadEventTypes() {
|
||||
fetch('/audit/types')
|
||||
.then(response => response.json())
|
||||
.then(types => {
|
||||
if (!types || types.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate the type filter dropdowns
|
||||
const typeFilter = document.getElementById('typeFilter');
|
||||
const exportTypeFilter = document.getElementById('exportTypeFilter');
|
||||
|
||||
// Clear existing options except the first one (All event types)
|
||||
while (typeFilter.options.length > 1) {
|
||||
typeFilter.remove(1);
|
||||
}
|
||||
|
||||
while (exportTypeFilter.options.length > 1) {
|
||||
exportTypeFilter.remove(1);
|
||||
}
|
||||
|
||||
// Add new options
|
||||
types.forEach(type => {
|
||||
// Main filter dropdown
|
||||
const option = document.createElement('option');
|
||||
option.value = type;
|
||||
option.textContent = type;
|
||||
typeFilter.appendChild(option);
|
||||
|
||||
// Export filter dropdown
|
||||
const exportOption = document.createElement('option');
|
||||
exportOption.value = type;
|
||||
exportOption.textContent = type;
|
||||
exportTypeFilter.appendChild(exportOption);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading event types:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to generate a palette of colors for charts
|
||||
function getChartColors(count, opacity = 0.6) {
|
||||
// Base colors - a larger palette than the default
|
||||
@ -981,9 +1273,13 @@
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
<!-- End of deprecated script -->
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
<!-- DO NOT REMOVE <th:block th:insert="~{fragments/footer.html :: footer}"></th:block> -->
|
||||
</div>
|
||||
|
||||
<!-- Debug console -->
|
||||
<div id="debug-console"></div>
|
||||
</body>
|
||||
</html>
|
@ -13,17 +13,11 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
public class CleanUrlInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final List<String> ALLOWED_PARAMS =
|
||||
Arrays.asList(
|
||||
"lang",
|
||||
"endpoint",
|
||||
"endpoints",
|
||||
"logout",
|
||||
"error",
|
||||
"errorOAuth",
|
||||
"file",
|
||||
"messageType",
|
||||
"infoMessage");
|
||||
private static final List<String> ALLOWED_PARAMS = Arrays.asList(
|
||||
"lang", "endpoint", "endpoints", "logout", "error", "errorOAuth", "file", "messageType", "infoMessage",
|
||||
"page", "size", "type", "principal", "startDate", "endDate"
|
||||
);
|
||||
|
||||
|
||||
@Override
|
||||
public boolean preHandle(
|
||||
|
Loading…
x
Reference in New Issue
Block a user