Add pagination to audit page
This commit is contained in:
@@ -34,6 +34,7 @@
|
|||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
<div class="empty" id="auditEmpty" style="display:none">Записей пока нет.</div>
|
<div class="empty" id="auditEmpty" style="display:none">Записей пока нет.</div>
|
||||||
|
<div class="pager" id="auditPager"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+40
-6
@@ -21,6 +21,10 @@ const ACTION_LABELS = {
|
|||||||
'backup.restore': 'Выполнено восстановление',
|
'backup.restore': 'Выполнено восстановление',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AUDIT_PAGE_SIZE = 50;
|
||||||
|
let auditPage = 1;
|
||||||
|
let auditTotal = 0;
|
||||||
|
|
||||||
function fmtTime(t) {
|
function fmtTime(t) {
|
||||||
try { return new Date(t).toLocaleString('ru'); } catch { return t || ''; }
|
try { return new Date(t).toLocaleString('ru'); } catch { return t || ''; }
|
||||||
}
|
}
|
||||||
@@ -36,20 +40,43 @@ function dt(t) {
|
|||||||
} catch { return esc(String(t).slice(0, 120)); }
|
} catch { return esc(String(t).slice(0, 120)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderPager(container, page, totalPages, onPageChange) {
|
||||||
|
if (totalPages <= 1) { container.innerHTML = ''; return; }
|
||||||
|
const pages = [];
|
||||||
|
const maxVisible = 5;
|
||||||
|
let start = Math.max(1, page - Math.floor(maxVisible / 2));
|
||||||
|
let end = Math.min(totalPages, start + maxVisible - 1);
|
||||||
|
if (end - start + 1 < maxVisible) start = Math.max(1, end - maxVisible + 1);
|
||||||
|
for (let i = start; i <= end; i++) pages.push(i);
|
||||||
|
let html = '';
|
||||||
|
if (page > 1) html += `<button data-page="${page - 1}">«</button>`;
|
||||||
|
if (start > 1) { html += `<button data-page="1">1</button>`; if (start > 2) html += `<span class="pager-ellipsis">…</span>`; }
|
||||||
|
for (const p of pages) html += `<button data-page="${p}"${p === page ? ' class="active"' : ''}>${p}</button>`;
|
||||||
|
if (end < totalPages) { if (end < totalPages - 1) html += `<span class="pager-ellipsis">…</span>`; html += `<button data-page="${totalPages}">${totalPages}</button>`; }
|
||||||
|
if (page < totalPages) html += `<button data-page="${page + 1}">»</button>`;
|
||||||
|
container.innerHTML = html;
|
||||||
|
container.querySelectorAll('button').forEach(b => b.addEventListener('click', () => onPageChange(parseInt(b.dataset.page, 10))));
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAudit() {
|
async function loadAudit() {
|
||||||
const el = document.getElementById('auditTable').querySelector('tbody');
|
const el = document.getElementById('auditTable').querySelector('tbody');
|
||||||
el.innerHTML = '<tr><td colspan="4" style="color:var(--muted)">Загрузка…</td></tr>';
|
el.innerHTML = '<tr><td colspan="4" style="color:var(--muted)">Загрузка…</td></tr>';
|
||||||
const res = await fetch(`${API}/api/audit?limit=500`, { headers: hdr() });
|
const p = new URLSearchParams();
|
||||||
|
p.set('limit', AUDIT_PAGE_SIZE);
|
||||||
|
p.set('offset', (auditPage - 1) * AUDIT_PAGE_SIZE);
|
||||||
|
const res = await fetch(`${API}/api/audit?${p}`, { headers: hdr() });
|
||||||
if (!res.ok) { el.innerHTML = '<tr><td colspan="4" style="color:var(--muted)">Ошибка загрузки</td></tr>'; return; }
|
if (!res.ok) { el.innerHTML = '<tr><td colspan="4" style="color:var(--muted)">Ошибка загрузки</td></tr>'; return; }
|
||||||
const rows = await res.json();
|
const rows = await res.json();
|
||||||
document.getElementById('auditCount').textContent = rows.length ? `Записей: ${rows.length}` : '';
|
// Total count for pagination (approximate: if we got less than page size, we're on last page)
|
||||||
|
const totalPages = rows.length < AUDIT_PAGE_SIZE ? auditPage : auditPage + 1;
|
||||||
|
auditTotal = totalPages;
|
||||||
|
document.getElementById('auditCount').textContent = rows.length ? `Записей на странице: ${rows.length}` : '';
|
||||||
const q = (document.getElementById('auditFilter').value || '').toLowerCase().trim();
|
const q = (document.getElementById('auditFilter').value || '').toLowerCase().trim();
|
||||||
const filtered = q ? rows.filter(r => (ACTION_LABELS[r.action] || r.action).toLowerCase().includes(q)) : rows;
|
const filtered = q ? rows.filter(r => (ACTION_LABELS[r.action] || r.action).toLowerCase().includes(q)) : rows;
|
||||||
if (!filtered.length) {
|
if (!filtered.length) {
|
||||||
document.getElementById('auditEmpty').style.display = 'block';
|
document.getElementById('auditEmpty').style.display = 'block';
|
||||||
el.innerHTML = '';
|
el.innerHTML = '';
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
document.getElementById('auditEmpty').style.display = 'none';
|
document.getElementById('auditEmpty').style.display = 'none';
|
||||||
el.innerHTML = filtered.map(r => `
|
el.innerHTML = filtered.map(r => `
|
||||||
<tr>
|
<tr>
|
||||||
@@ -59,12 +86,19 @@ async function loadAudit() {
|
|||||||
<td style="color:var(--muted);white-space:nowrap">${esc(r.ip || '')}</td>
|
<td style="color:var(--muted);white-space:nowrap">${esc(r.ip || '')}</td>
|
||||||
</tr>`).join('');
|
</tr>`).join('');
|
||||||
}
|
}
|
||||||
|
renderPager(document.getElementById('auditPager'), auditPage, auditTotal, goAuditPage);
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('auditFilter').addEventListener('input', loadAudit);
|
function goAuditPage(p) {
|
||||||
|
auditPage = p;
|
||||||
|
loadAudit();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('auditFilter').addEventListener('input', () => { auditPage = 1; loadAudit(); });
|
||||||
document.getElementById('auditRefreshBtn').addEventListener('click', loadAudit);
|
document.getElementById('auditRefreshBtn').addEventListener('click', loadAudit);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
if (await checkAuth()) {
|
if (await requireAdminPage()) {
|
||||||
buildSidebar(document.body.dataset.page);
|
buildSidebar(document.body.dataset.page);
|
||||||
loadAudit();
|
loadAudit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -607,11 +607,12 @@ app.put('/api/settings', requireAdmin, async (req, res) => {
|
|||||||
|
|
||||||
app.get('/api/audit', requireAdmin, async (req, res) => {
|
app.get('/api/audit', requireAdmin, async (req, res) => {
|
||||||
const limit = Math.min(parseInt(req.query.limit, 10) || 100, 1000);
|
const limit = Math.min(parseInt(req.query.limit, 10) || 100, 1000);
|
||||||
|
const offset = Math.max(parseInt(req.query.offset, 10) || 0, 0);
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT a.id, a.action, a.target, a.ip, a.created_at, a.user_id, u.username AS user_name
|
`SELECT a.id, a.action, a.target, a.ip, a.created_at, a.user_id, u.username AS user_name
|
||||||
FROM audit_log a LEFT JOIN users u ON u.id = a.user_id
|
FROM audit_log a LEFT JOIN users u ON u.id = a.user_id
|
||||||
ORDER BY a.id DESC LIMIT $1`,
|
ORDER BY a.id DESC LIMIT $1 OFFSET $2`,
|
||||||
[limit]
|
[limit, offset]
|
||||||
);
|
);
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user