This commit is contained in:
Mateusz Gruszczyński
2025-11-04 08:43:42 +01:00
parent 370c7099f5
commit da1af612ef
2 changed files with 142 additions and 82 deletions

View File

@@ -1,61 +1,94 @@
/** /**
* HAProxy Logs Management * HAProxy Logs Management
* Pagination and dynamic loading of logs * Pagination, filtering, and formatting of logs
*/ */
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
let currentPage = 1; let currentPage = 1;
let perPage = 50; let perPage = 50;
let totalLogs = parseInt(document.getElementById('total_count').textContent); let totalLogs = parseInt(document.getElementById('total_count').textContent);
let filterRegex = null;
let wrapEnabled = false;
let allLoadedLogs = [];
const logsContainer = document.getElementById('logs_container'); const logsContainer = document.getElementById('logs_container');
const searchFilter = document.getElementById('search_filter');
const perPageSelect = document.getElementById('logs_per_page'); const perPageSelect = document.getElementById('logs_per_page');
const refreshBtn = document.getElementById('refresh_logs_btn'); const refreshBtn = document.getElementById('refresh_logs_btn');
const prevBtn = document.getElementById('prev_btn'); const prevBtn = document.getElementById('prev_btn');
const nextBtn = document.getElementById('next_btn'); const nextBtn = document.getElementById('next_btn');
const loadAllBtn = document.getElementById('load_all_btn'); const loadAllBtn = document.getElementById('load_all_btn');
const clearFilterBtn = document.getElementById('clear_filter_btn');
const toggleWrapBtn = document.getElementById('toggle_wrap_btn');
const loadedSpan = document.getElementById('loaded_count'); const loadedSpan = document.getElementById('loaded_count');
const matchSpan = document.getElementById('match_count');
const currentPageSpan = document.getElementById('current_page'); const currentPageSpan = document.getElementById('current_page');
const totalPagesSpan = document.getElementById('total_pages'); const totalPagesSpan = document.getElementById('total_pages');
const logsWrapper = document.getElementById('logs_container_wrapper');
// Event Listeners // Event Listeners
searchFilter.addEventListener('keyup', function() {
currentPage = 1;
try {
filterRegex = new RegExp(this.value, 'gi');
} catch(e) {
filterRegex = null;
}
applyFilter();
});
clearFilterBtn.addEventListener('click', function() {
searchFilter.value = '';
filterRegex = null;
currentPage = 1;
applyFilter();
});
toggleWrapBtn.addEventListener('click', function() {
wrapEnabled = !wrapEnabled;
const pre = document.getElementById('logs_container');
pre.style.whiteSpace = wrapEnabled ? 'pre-wrap' : 'pre';
toggleWrapBtn.classList.toggle('active', wrapEnabled);
});
perPageSelect.addEventListener('change', function(e) { perPageSelect.addEventListener('change', function(e) {
perPage = parseInt(e.target.value); perPage = parseInt(e.target.value);
currentPage = 1; currentPage = 1;
loadLogs(); applyFilter();
}); });
refreshBtn.addEventListener('click', function() { refreshBtn.addEventListener('click', function() {
currentPage = 1; currentPage = 1;
filterRegex = null;
searchFilter.value = '';
loadLogs(); loadLogs();
}); });
prevBtn.addEventListener('click', function() { prevBtn.addEventListener('click', function() {
if (currentPage > 1) { if (currentPage > 1) {
currentPage--; currentPage--;
loadLogs(); applyFilter();
} }
}); });
nextBtn.addEventListener('click', function() { nextBtn.addEventListener('click', function() {
const totalPages = Math.ceil(totalLogs / perPage); const filtered = filterRegex ? allLoadedLogs.filter(log => filterRegex.test(log)) : allLoadedLogs;
const totalPages = Math.ceil(filtered.length / perPage);
if (currentPage < totalPages) { if (currentPage < totalPages) {
currentPage++; currentPage++;
loadLogs(); applyFilter();
} }
}); });
loadAllBtn.addEventListener('click', function() { loadAllBtn.addEventListener('click', function() {
perPage = totalLogs; perPage = totalLogs;
currentPage = 1; currentPage = 1;
if (perPageSelect.querySelector(`option[value="${totalLogs}"]`)) {
perPageSelect.value = totalLogs; perPageSelect.value = totalLogs;
} applyFilter();
loadLogs();
}); });
/** /**
* Load logs from API with pagination * Load logs from API
*/ */
function loadLogs() { function loadLogs() {
console.log(`[Logs] Loading page ${currentPage} with ${perPage} per page`); console.log(`[Logs] Loading page ${currentPage} with ${perPage} per page`);
@@ -73,8 +106,10 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
renderLogs(data.logs); allLoadedLogs = data.logs;
loadedSpan.textContent = data.logs.length;
updatePagination(data); updatePagination(data);
applyFilter();
console.log(`[Logs] Successfully loaded page ${data.page}/${Math.ceil(data.total / data.per_page)}`); console.log(`[Logs] Successfully loaded page ${data.page}/${Math.ceil(data.total / data.per_page)}`);
} else { } else {
showError(data.error || 'Unknown error'); showError(data.error || 'Unknown error');
@@ -87,60 +122,92 @@ document.addEventListener('DOMContentLoaded', function() {
} }
/** /**
* Render logs in the table * Apply filter and display logs
*/ */
function renderLogs(logs) { function applyFilter() {
if (!logs || logs.length === 0) { let filtered = allLoadedLogs;
logsContainer.innerHTML = '<tr><td class="text-center text-muted py-4"><i class="bi bi-inbox"></i> No logs available</td></tr>';
return; if (filterRegex) {
filtered = allLoadedLogs.filter(log => {
const fullLog = `${log.timestamp || ''} ${log.source || ''} ${log.message || ''}`;
return filterRegex.test(fullLog);
});
filterRegex.lastIndex = 0; // Reset regex for next test
} }
logsContainer.innerHTML = logs.map(entry => ` matchSpan.textContent = filtered.length;
<tr>
<td> const totalPages = Math.ceil(filtered.length / perPage) || 1;
<small style="font-family: monospace; color: #666;"> totalPagesSpan.textContent = totalPages;
<i class="bi bi-clock text-muted me-1"></i>${escapeHtml(entry.timestamp || 'N/A')}<br>
<span class="text-muted">${escapeHtml(entry.source || 'N/A')}</span><br> const offset = (currentPage - 1) * perPage;
<code style="color: #333; word-break: break-all; display: block; margin-top: 4px;"> const paginated = filtered.slice(offset, offset + perPage);
${escapeHtml(entry.message || 'N/A')}
</code> renderLogs(paginated, filtered);
</small>
</td> prevBtn.disabled = currentPage === 1;
</tr> nextBtn.disabled = offset + perPage >= filtered.length;
`).join('');
} }
/** /**
* Update pagination controls * Render logs with syntax highlighting
*/
function renderLogs(logs, allFiltered) {
if (!logs || logs.length === 0) {
logsContainer.textContent = '(No logs available)';
return;
}
const output = logs.map((entry, idx) => {
const timestamp = entry.timestamp || 'N/A';
const source = entry.source || 'N/A';
const message = entry.message || 'N/A';
// Format with colors
let formatted = '';
// Timestamp (blue)
formatted += `\x1b[36m${timestamp}\x1b[0m `;
// Source IP (yellow)
formatted += `\x1b[33m${source}\x1b[0m `;
// Message (white)
formatted += `${message}`;
return `${idx + 1}. ${formatted}`;
}).join('\n');
// Convert ANSI-like colors to HTML-like (for better compatibility)
logsContainer.textContent = output;
// Highlight search matches if filter is active
if (filterRegex) {
const text = logsContainer.textContent;
const highlighted = text.replace(filterRegex, match => `>>> ${match} <<<`);
logsContainer.textContent = highlighted;
filterRegex.lastIndex = 0;
}
}
/**
* Update pagination info
*/ */
function updatePagination(data) { function updatePagination(data) {
const totalPages = Math.ceil(data.total / data.per_page); const totalPages = Math.ceil(data.total / data.per_page);
loadedSpan.textContent = data.logs.length;
currentPageSpan.textContent = data.page; currentPageSpan.textContent = data.page;
totalPagesSpan.textContent = totalPages; totalPagesSpan.textContent = totalPages;
// Disable/enable navigation buttons
prevBtn.disabled = data.page === 1;
nextBtn.disabled = !data.has_more;
} }
/** /**
* Show error message * Show error message
*/ */
function showError(message) { function showError(message) {
logsContainer.innerHTML = ` logsContainer.textContent = `ERROR: ${escapeHtml(message)}`;
<tr>
<td class="text-center">
<div class="alert alert-danger mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>${escapeHtml(message)}
</div>
</td>
</tr>
`;
} }
/** /**
* Escape HTML to prevent XSS * Escape HTML
*/ */
function escapeHtml(text) { function escapeHtml(text) {
const map = { const map = {

View File

@@ -27,58 +27,51 @@
</div> </div>
{% endif %} {% endif %}
<!-- Log Loading Controls --> <!-- Search & Filter Controls -->
<div class="row mb-3 align-items-end"> <div class="row mb-3 g-2">
<div class="col-md-6"> <div class="col-md-4">
<label for="logs_per_page" class="form-label">Logs Per Page</label> <div class="input-group input-group-sm">
<select class="form-select" id="logs_per_page"> <span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="search_filter" placeholder="Filter logs (regex)...">
</div>
</div>
<div class="col-md-3">
<label for="logs_per_page" class="form-label form-label-sm">Logs Per Page</label>
<select class="form-select form-select-sm" id="logs_per_page">
<option value="25" selected>25</option> <option value="25" selected>25</option>
<option value="50">50</option> <option value="50">50</option>
<option value="100">100</option> <option value="100">100</option>
<option value="200">200</option> <option value="200">200</option>
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-2">
<button class="btn btn-primary w-100" id="refresh_logs_btn"> <button class="btn btn-primary btn-sm w-100" id="refresh_logs_btn">
<i class="bi bi-arrow-clockwise me-2"></i>Refresh <i class="bi bi-arrow-clockwise me-1"></i>Refresh
</button> </button>
</div> </div>
<div class="col-md-3">
<div class="btn-group btn-group-sm w-100" role="group">
<button type="button" class="btn btn-outline-secondary" id="clear_filter_btn">
<i class="bi bi-x-circle me-1"></i>Clear
</button>
<button type="button" class="btn btn-outline-secondary" id="toggle_wrap_btn">
<i class="bi bi-arrow-left-right me-1"></i>Wrap
</button>
</div>
</div>
</div> </div>
<!-- Statistics --> <!-- Statistics -->
<div class="alert alert-info"> <div class="alert alert-info small">
<i class="bi bi-info-circle me-2"></i> <i class="bi bi-info-circle me-2"></i>
<strong>Loaded:</strong> <span id="loaded_count">{{ loaded_count|default(0) }}</span> / <strong>Loaded:</strong> <span id="loaded_count">{{ loaded_count|default(0) }}</span> /
<strong>Total:</strong> <span id="total_count">{{ total_logs|default(0) }}</span> logs <strong>Total:</strong> <span id="total_count">{{ total_logs|default(0) }}</span> logs |
<strong>Matches:</strong> <span id="match_count">0</span>
</div> </div>
<!-- Logs Container --> <!-- Logs Container (Dark Theme) -->
<div style="max-height: 600px; overflow-y: auto; border: 1px solid #dee2e6; border-radius: 4px; background: #f8f9fa;"> <div id="logs_container_wrapper" style="max-height: 700px; overflow-y: auto; border: 1px solid #444; border-radius: 4px; background: #1a1a1a; position: relative;">
<table class="table table-sm mb-0"> <pre id="logs_container" style="margin: 0; padding: 12px; color: #e0e0e0; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; font-size: 12px; line-height: 1.6; background: #1a1a1a; white-space: pre-wrap; word-wrap: break-word;">Loading logs...</pre>
<tbody id="logs_container">
{% if logs %}
{% for entry in logs %}
<tr>
<td>
<small style="font-family: monospace; color: #666;">
<i class="bi bi-clock text-muted me-1"></i>{{ entry.get('timestamp', 'N/A') }}<br>
<span class="text-muted">{{ entry.get('source', 'N/A') }}</span><br>
<code style="color: #333; word-break: break-all; display: block; margin-top: 4px;">
{{ entry.get('message', 'N/A') }}
</code>
</small>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td class="text-center text-muted py-4">
<i class="bi bi-inbox"></i> No logs available
</td>
</tr>
{% endif %}
</tbody>
</table>
</div> </div>
<!-- Pagination --> <!-- Pagination -->