Files
pve-ha-web/static/js/tables.js
Mateusz Gruszczyński 2682a0ff4f refator_comm1
2025-10-18 20:58:51 +02:00

192 lines
9.9 KiB
JavaScript

import { $, safe, ensureArr, badge, rowHTML, setRows, pct, humanBytes, fmtSeconds, kvGrid, showToast } from './helpers.js';
import { api } from './api.js';
import { renderVmDetailCard } from './vmDetail.js';
import { renderNodeDetailCard } from './nodeDetail.js';
// DOM refs
export const refs = {
nodeInput: $('#node'),
healthDot: $('#healthDot'), healthTitle: $('#healthTitle'), healthSub: $('#healthSub'),
qSummary: $('#q-summary'), qCardsWrap: $('#q-cards'), unitsBox: $('#units'), replBox: $('#repl'),
tblHaRes: $('#ha-res'), tblHaStatus: $('#ha-status'), // (tblHaStatus nieużywany, sekcja wycięta w HTML)
tblNodes: $('#nodes'), tblNonHA: $('#nonha'),
pvecmPre: $('#pvecm'), cfgtoolPre: $('#cfgtool'), footer: $('#footer')
};
// Health
export function setHealth(ok, vq, unitsActive) {
refs.healthDot.classList.toggle('ok', !!ok);
refs.healthDot.classList.toggle('bad', !ok);
refs.healthTitle.textContent = ok ? 'HA: OK' : 'HA: PROBLEM';
refs.healthSub.textContent = `Quorate=${String(vq.quorate)} | units=${unitsActive ? 'active' : 'inactive'} | members=${safe(vq.members)} | quorum=${safe(vq.quorum)}/${safe(vq.expected)}`;
}
// Cluster cards
export function renderClusterCards(arr) {
const a = ensureArr(arr); refs.qCardsWrap.innerHTML = '';
if (!a.length) { refs.qCardsWrap.innerHTML = badge('No data','dark'); return; }
const cluster = a.find(x => x.type === 'cluster') || {};
const qB = cluster.quorate ? badge('Quorate: yes','ok') : badge('Quorate: no','err');
refs.qCardsWrap.insertAdjacentHTML('beforeend', `
<div class="col-12">
<div class="card border-0 shadow-sm"><div class="card-body d-flex flex-wrap align-items-center gap-3">
<div class="fw-bold">${safe(cluster.name)}</div>
<div class="text-muted small">id: ${safe(cluster.id)}</div><div class="vr"></div>
<div>${qB}</div><div class="vr"></div>
<div class="small">nodes: <strong>${safe(cluster.nodes)}</strong></div>
<div class="small">version: <strong>${safe(cluster.version)}</strong></div>
</div></div>
</div>`);
const nodes = a.filter(x => x.type === 'node');
const rows = nodes.map(n => {
const online = n.online ? badge('online','ok') : badge('offline','err');
const local = n.local ? ' ' + badge('local','info') : '';
return rowHTML([safe(n.name), online + local, safe(n.ip), safe(n.nodeid), safe(n.level)]);
});
refs.qCardsWrap.insertAdjacentHTML('beforeend', `
<div class="col-12"><div class="card border-0"><div class="card-body table-responsive pt-2">
<table class="table table-sm table-striped align-middle table-nowrap">
<thead><tr><th>Node</th><th>Status</th><th>IP</th><th>NodeID</th><th>Level</th></tr></thead>
<tbody>${rows.join('')}</tbody>
</table>
</div></div></div>`);
}
export function renderUnits(units) {
refs.unitsBox.innerHTML = '';
if (!units || !Object.keys(units).length) { refs.unitsBox.innerHTML = badge('No data','dark'); return; }
const map = { active:'ok', inactive:'err', failed:'err', activating:'warn' };
Object.entries(units).forEach(([k, v]) => refs.unitsBox.insertAdjacentHTML('beforeend', `<span class="me-2">${badge(k, map[v] || 'dark')}</span>`));
}
// Replication — ALL NODES
export function renderReplicationTable(repAll) {
const arr = ensureArr(repAll.jobs);
if (!arr.length) { refs.replBox && (refs.replBox.innerHTML = '<span class="text-muted small">No replication jobs</span>'); return; }
const rows = arr.map(x => {
const en = /^yes$/i.test(x.enabled) ? badge('Yes','ok') : badge('No','err');
const st = /^ok$/i.test(x.state) ? badge(x.state,'ok') : badge(x.state,'err');
const fc = x.fail > 0 ? badge(String(x.fail),'err') : badge(String(x.fail),'ok');
return rowHTML([safe(x.node), safe(x.job), en, safe(x.target), safe(x.last), safe(x.next), safe(x.dur), fc, st]);
});
refs.replBox.innerHTML = `<div class="table-responsive"><table class="table table-sm table-striped align-middle table-nowrap">
<thead><tr><th>Node</th><th>JobID</th><th>Enabled</th><th>Target</th><th>LastSync</th><th>NextSync</th><th>Duration</th><th>FailCount</th><th>State</th></tr></thead>
<tbody>${rows.join('')}</tbody></table></div>`;
}
// HA resources (expand)
export function renderHAResources(list) {
const tbody = refs.tblHaRes.querySelector('tbody');
const arr = ensureArr(list); const rows = [];
arr.forEach(x => {
const st = x.state || '—';
const stB = /start/i.test(st) ? badge(st,'ok') : (/stop/i.test(st) ? badge(st,'err') : badge(st,'dark'));
const sid = safe(x.sid);
rows.push(rowHTML([`<span class="chev"></span>`, sid, stB, safe(x.node), safe(x.group), safe(x.flags || x.comment)], `class="expandable vm-row" data-sid="${sid}"`));
rows.push(`<tr class="vm-detail d-none"><td colspan="6"><div class="spinner-border spinner-border-sm me-2 d-none"></div><div class="vm-json">—</div></td></tr>`);
});
setRows(tbody, rows.length ? rows : [rowHTML(['—','—','—','—','—','—'])]);
Array.from(refs.tblHaRes.querySelectorAll('tr.vm-row')).forEach((tr, i) => {
tr.onclick = async () => {
const detailRow = refs.tblHaRes.querySelectorAll('tr.vm-detail')[i];
const content = detailRow.querySelector('.vm-json');
const spin = detailRow.querySelector('.spinner-border');
const open = detailRow.classList.contains('d-none');
// collapse all
refs.tblHaRes.querySelectorAll('tr.vm-detail').forEach(r => r.classList.add('d-none'));
refs.tblHaRes.querySelectorAll('tr.vm-row').forEach(r => r.classList.remove('expanded'));
if (open) {
detailRow.classList.remove('d-none'); tr.classList.add('expanded');
spin.classList.remove('d-none');
const sid = tr.getAttribute('data-sid');
try { const d = await api.vmDetail(sid); content.innerHTML = renderVmDetailCard(d); }
catch (e) { content.textContent = 'ERROR: ' + e; }
spin.classList.add('d-none');
}
};
});
}
// Non-HA VM/CT (expand)
export async function renderNonHA() {
const r = await api.listNonHA();
const arr = ensureArr(r.nonha);
const tbody = refs.tblNonHA.querySelector('tbody');
if (!arr.length) { setRows(tbody, [rowHTML(['','No non-HA VMs/CTs','','','',''])]); return; }
const rows = [];
arr.forEach(x => {
const sid = safe(x.sid), type = safe(x.type), name = safe(x.name), node = safe(x.node);
const st = /running/i.test(x.status || '') ? badge(x.status,'ok') : badge(x.status||'—','dark');
rows.push(rowHTML([`<span class="chev"></span>`, sid, type, name, node, st], `class="expandable vm-row" data-sid="${sid}"`));
rows.push(`<tr class="vm-detail d-none"><td colspan="6"><div class="spinner-border spinner-border-sm me-2 d-none"></div><div class="vm-json">—</div></td></tr>`);
});
setRows(tbody, rows);
Array.from(tbody.querySelectorAll('tr.vm-row')).forEach((tr, i) => {
tr.onclick = async () => {
const detailRow = tbody.querySelectorAll('tr.vm-detail')[i];
const content = detailRow.querySelector('.vm-json');
const spin = detailRow.querySelector('.spinner-border');
const open = detailRow.classList.contains('d-none');
tbody.querySelectorAll('tr.vm-detail').forEach(r => r.classList.add('d-none'));
tbody.querySelectorAll('tr.vm-row').forEach(r => r.classList.remove('expanded'));
if (open) {
detailRow.classList.remove('d-none'); tr.classList.add('expanded');
spin.classList.remove('d-none');
const sid = tr.getAttribute('data-sid');
try { const d = await api.vmDetail(sid); content.innerHTML = renderVmDetailCard(d); }
catch (e) { content.textContent = 'ERROR: ' + e; }
spin.classList.add('d-none');
}
};
});
}
// Nodes table (expand)
export function renderNodesTable(nodes) {
const tbody = refs.tblNodes.querySelector('tbody');
const nrows = ensureArr(nodes).map(n => {
const isOn = /online|running/i.test(n.status || '') ||
/online/i.test(n.hastate || '') || (n.uptime > 0) ||
(n.cpu != null && n.maxcpu != null) || (n.mem != null && n.maxmem != null);
const statusTxt = isOn ? 'online' : (n.status || 'offline');
const sB = isOn ? badge(statusTxt,'ok') : badge(statusTxt,'err');
const mem = (n.mem != null && n.maxmem) ? `${humanBytes(n.mem)} / ${humanBytes(n.maxmem)} (${pct(n.mem / n.maxmem)})` : '—';
const rfs = (n.rootfs != null && n.maxrootfs) ? `${humanBytes(n.rootfs)} / ${humanBytes(n.maxrootfs)} (${pct(n.rootfs / n.maxrootfs)})` : '—';
const load = (n.loadavg != null) ? String(n.loadavg) : '—';
const cpu = (n.cpu != null) ? pct(n.cpu) : '—';
const main = `<tr class="expandable node-row" data-node="${safe(n.node)}">
<td class="chev"></td><td class="sticky-col fw-semibold">${safe(n.node)}</td>
<td>${sB}</td><td>${cpu}</td><td>${load}</td><td>${mem}</td><td>${rfs}</td><td>${fmtSeconds(n.uptime)}</td>
</tr>`;
const detail = `<tr class="node-detail d-none"><td colspan="8">
<div class="spinner-border spinner-border-sm me-2 d-none"></div><div class="node-json">—</div>
</td></tr>`;
return main + detail;
});
setRows(tbody, nrows);
Array.from(tbody.querySelectorAll('tr.node-row')).forEach((tr, i) => {
tr.onclick = async () => {
const detailRow = tbody.querySelectorAll('tr.node-detail')[i];
const content = detailRow.querySelector('.node-json');
const spin = detailRow.querySelector('.spinner-border');
const open = detailRow.classList.contains('d-none');
tbody.querySelectorAll('tr.node-detail').forEach(r => r.classList.add('d-none'));
tbody.querySelectorAll('tr.node-row').forEach(r => r.classList.remove('expanded'));
if (open) {
detailRow.classList.remove('d-none'); tr.classList.add('expanded');
spin.classList.remove('d-none');
const name = tr.getAttribute('data-node');
try { const d = await api.nodeDetail(name); content.innerHTML = renderNodeDetailCard(d); }
catch (e) { content.textContent = 'ERROR: ' + e; }
spin.classList.add('d-none');
}
};
});
}