refator_comm1
This commit is contained in:
191
static/js/tables.js
Normal file
191
static/js/tables.js
Normal file
@@ -0,0 +1,191 @@
|
||||
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');
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user