Files
podman-mvp/webui/html/assets/js/tabs/containers.js
T
2026-03-04 07:48:58 +01:00

398 lines
13 KiB
JavaScript

// ---- Pods ----
async function podAction(action, name) {
try {
const res = await api(`/pods/actions/${encodeURIComponent(action)}/${encodeURIComponent(name)}`, 'POST');
showModal(`Pod ${action}: ${name}`, JSON.stringify(res, null, 2));
await fetchContainers(); // refresh pods + containers view in één keer
} catch (e) {
showModal(`Pod ${action} fout`, e.stack || e.message);
}
}
// ---- Containers ----
function _podKey(c) {
return (c.PodName && String(c.PodName).trim()) ? String(c.PodName).trim() : '(geen pod)';
}
function _cmpStr(a, b) {
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
}
function _isCollapsed(pod) {
return localStorage.getItem('pod_group_collapsed:' + pod) === '1';
}
function _setCollapsed(pod, v) {
localStorage.setItem('pod_group_collapsed:' + pod, v ? '1' : '0');
}
function renderActionsDropdown(menuId, actionFn, targetEsc) {
// actionFn is string: "containerAction" of "podAction"
// targetEsc is al esc(...) dus veilig in onclick
return `
<span class="actions-menu">
<button class="btn icon" title="Acties" onclick="toggleMenu('${menuId}')">⋮</button>
<div class="menuPanel" id="${menuId}">
<button class="menuItem ok" onclick="${actionFn}('start','${targetEsc}'); closeAllMenus();">Start</button>
<button class="menuItem warn" onclick="${actionFn}('restart','${targetEsc}'); closeAllMenus();">Restart</button>
<button class="menuItem bad" onclick="${actionFn}('stop','${targetEsc}'); closeAllMenus();">Stop</button>
</div>
</span>
`;
}
function renderContainerRow(c) {
const name = (c.Names && c.Names[0]) ? c.Names[0] : (c.Names || c.Name || c.name || '');
const status = c.Status || c.State || c.state || '';
const podName = c.PodName || '-';
const image = c.Image || c.image || '';
const managed = c._dashboard_source || 'podman';
const menuId = `menu-${cssSafeId('c:' + normalizeContainerName(name))}`;
const inPod = !!(c.PodName && String(c.PodName).trim());
const ports = inPod
? '' // verberg bij pod-containers
: ((c._dashboard_published_ports || []).join(", ")
|| (c.Ports || []).map(p => `${p.host_port}:${p.container_port}`).join(", "));
return `
<tr>
<td><strong>${esc(name)}</strong></td>
<td>${badgeFromStatus(status)}</td>
<td>${podName}</td>
<td class="muted">${esc(image)}</td>
<td>${badgeFromStatus(managed)}</td>
<td class="muted num" id="cpu-${cssSafeId(normalizeContainerName(name))}">-</td>
<td class="muted num" id="mem-${cssSafeId(normalizeContainerName(name))}">-</td>
<td>${ports || '-'}</td>
<td>
<div class="flex">
<button class="btn icon" title="Inspect" onclick="containerInspect(decodeURIComponent('${encodeURIComponent(name)}'))">🔍</button>
<button class="btn icon" title="Logs" onclick="containerLogs(decodeURIComponent('${encodeURIComponent(name)}'))">📄</button>
${renderActionsDropdown(menuId, 'containerAction', esc(name))}
</div>
</td>
</tr>
`;
}
let containersC2P = new Map();
function renderContainersGrouped(list, tbody, podStatus) {
const groups = new Map();
for (const c of (list || [])) {
const k = _podKey(c);
if (!groups.has(k)) groups.set(k, []);
groups.get(k).push(c);
}
for (const podName of Object.keys(podStatus || {})) {
if (!groups.has(podName)) groups.set(podName, []);
}
const keys = Array.from(groups.keys()).sort((a, b) => {
if (a === '(geen pod)' && b !== '(geen pod)') return 1;
if (b === '(geen pod)' && a !== '(geen pod)') return -1;
return _cmpStr(a, b);
});
containersC2P = new Map();
let html = '';
for (const pod of keys) {
const items = groups.get(pod) || [];
items.sort((a, b) =>
_cmpStr(
(a.Names && a.Names[0]) ? a.Names[0] : (a.Name || ''),
(b.Names && b.Names[0]) ? b.Names[0] : (b.Name || '')
)
);
const collapsed = _isCollapsed(pod);
const isRealPod = (pod !== '(geen pod)');
const total = items.length;
const podMenuId = `menu-${cssSafeId('p:' + pod)}`;
let podPortsText = "-";
if (isRealPod) {
const podPorts = Array.from(new Set(
items.flatMap(c => (c._dashboard_published_ports || []))
)).sort((a, b) => _cmpStr(a, b));
podPortsText = podPorts.length ? podPorts.join(", ") : "-";
}
let cls = 'muted';
let label = '-';
if (total > 0) {
const running = items.filter(x => {
const s = (x.Status || x.State || '').toLowerCase();
return s === 'running';
}).length;
label = `${running}/${total}`;
if (running === total) cls = 'ok';
else if (running === 0) cls = 'bad';
else cls = 'warn';
} else {
const ps = String((podStatus && podStatus[pod]) || '').toLowerCase();
if (ps === 'active') { cls = 'ok'; label = 'active'; }
else if (ps === 'inactive') { cls = 'bad'; label = 'inactive'; }
else { cls = 'muted'; label = ps || 'unknown'; }
}
html += `
<tr class="pod-group-row">
<td>
<span class="pod-toggle" data-pod="${encodeURIComponent(pod)}" style="cursor:pointer; user-select:none;">
${collapsed ? '▶' : '▼'} <b>${esc(pod)}</b>
</span>
</td>
<td>
<span class="btn small ${cls}">
${label}
</span>
</td>
<td class="muted">-</td>
<td class="muted">-</td>
<td>${isRealPod ? '<span class="btn small muted">pod</span>' : '<span class="muted">-</span>'}</td>
<td class="muted num" id="podcpu-${cssSafeId(pod)}" data-podstatus="${esc((podStatus && podStatus[pod]) || '')}">-</td>
<td class="muted num" id="podmem-${cssSafeId(pod)}" data-podstatus="${esc((podStatus && podStatus[pod]) || '')}">-</td>
<td class="mono">${esc(podPortsText)}</td>
<td>
${isRealPod ? `
<div class="flex">
<!-- placeholders voor uitlijning -->
<button class="btn icon" disabled style="visibility:hidden;" aria-hidden="true" tabindex="-1">🔍</button>
<button class="btn icon" disabled style="visibility:hidden;" aria-hidden="true" tabindex="-1">📄</button>
${renderActionsDropdown(podMenuId, 'podAction', esc(pod))}
</div>
` : '<span class="muted">-</span>'}
</td>
</tr>
`;
for (const c of items) {
const cname = normalizeContainerName((c.Names && c.Names[0]) ? c.Names[0] : (c.Names || c.Name || c.name || ''));
if (cname) containersC2P.set(cname, pod);
const row = renderContainerRow(c).replace(
'<tr',
`<tr class="pod-item-row" data-pod="${encodeURIComponent(pod)}"${collapsed ? ' style="display:none;"' : ''}`
);
html += row;
}
}
tbody.innerHTML = html;
tbody.onclick = (ev) => {
const t = ev.target.closest('.pod-toggle');
if (!t) return;
const podEnc = t.getAttribute('data-pod') || '';
const pod = decodeURIComponent(podEnc);
const isNowCollapsed = !_isCollapsed(pod);
_setCollapsed(pod, isNowCollapsed);
tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(podEnc)}"]`).forEach(r => {
r.style.display = isNowCollapsed ? 'none' : '';
});
t.innerHTML =
`${isNowCollapsed ? '▶' : '▼'} <b>${pod}</b> ` +
`<span style="opacity:.7;">(${tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(podEnc)}"]`).length})</span>`;
};
}
async function fetchContainers() {
const [containers, pods] = await Promise.all([
api('/containers-dashboard', 'GET'),
api('/pods-dashboard', 'GET')
]);
const list = Array.isArray(containers) ? containers : (containers?.containers || []);
document.getElementById('countContainers').textContent = list.length;
if (typeof window.updateNavCount === 'function') {
window.updateNavCount('countNavContainers', list.length);
}
const podsList = Array.isArray(pods) ? pods : [];
const podStatus = {};
for (const p of podsList) {
const n = p?.Name || p?.name;
if (!n) continue;
podStatus[n] = (p?.Status || p?.status || '').toLowerCase(); // "active"/"inactive"/...
}
const tbody = document.getElementById('containersTbody');
if (!tbody) return;
renderContainersGrouped(list, tbody, podStatus);
}
let containersDashboardStatsTimer = null;
let containersDashboardStatsInFlight = false;
function startContainersDashboardStatsPoll() {
if (containersDashboardStatsTimer) return;
// immediate tick, daarna elke 1s
pollContainersDashboardStatsOnce();
containersDashboardStatsTimer = setInterval(pollContainersDashboardStatsOnce, 1000);
}
function stopContainersDashboardStatsPoll() {
if (!containersDashboardStatsTimer) return;
clearInterval(containersDashboardStatsTimer);
containersDashboardStatsTimer = null;
resetPodTotals();
}
async function pollContainersDashboardStatsOnce() {
if (containersDashboardStatsInFlight) return;
containersDashboardStatsInFlight = true;
try {
const containers = await api('/containers-dashboard', 'GET');
const list = Array.isArray(containers) ? containers : (containers?.containers || []);
// totals per pod voor deze poll tick
const podCpu = new Map(); // podName -> cpuPct sum
const podMem = new Map(); // podName -> memBytes sum
const podMemPct = new Map(); // podName -> memPct sum
for (const c of (list || [])) {
const cname = normalizeContainerName((c?.Names && c.Names[0]) ? c.Names[0] : (c?.Names || c?.Name || c?.name || ''));
if (!cname) continue;
const key = cssSafeId(cname);
const cpuRaw = c?._dashboard_cpu;
const memBytesRaw = c?._dashboard_mem_usage;
const memPctRaw = c?._dashboard_mem_perc;
const cpuPct = Number(cpuRaw);
const memBytes = Number(memBytesRaw);
const memPct = Number(memPctRaw);
const pod = containersC2P.get(cname);
if (pod) {
if (Number.isFinite(cpuPct)) podCpu.set(pod, (podCpu.get(pod) || 0) + cpuPct);
if (Number.isFinite(memBytes)) podMem.set(pod, (podMem.get(pod) || 0) + memBytes);
if (Number.isFinite(memPct)) podMemPct.set(pod, (podMemPct.get(pod) || 0) + memPct);
}
const cpuEl = document.getElementById(`cpu-${key}`);
if (cpuEl) cpuEl.textContent = Number.isFinite(cpuPct) ? (cpuPct.toFixed(2) + "%") : "-";
const memEl = document.getElementById(`mem-${key}`);
if (memEl) {
if (Number.isFinite(memBytes) && Number.isFinite(memPct)) {
memEl.textContent = `${formatBytes(memBytes)} (${memPct.toFixed(1)}%)`;
} else {
memEl.textContent = "-";
}
}
}
for (const [pod, cpuSum] of podCpu.entries()) {
const el = document.getElementById(`podcpu-${cssSafeId(pod)}`);
if (el) {
el.textContent = cpuSum.toFixed(2) + "%";
el.classList.remove('stale');
}
}
for (const [pod, memSum] of podMem.entries()) {
const el = document.getElementById(`podmem-${cssSafeId(pod)}`);
if (el) {
const mp = podMemPct.get(pod) || 0;
el.textContent = `${formatBytes(memSum)} (${Number(mp).toFixed(1)}%)`;
el.classList.remove('stale');
}
}
} catch (e) {
resetPodTotals();
} finally {
containersDashboardStatsInFlight = false;
}
}
function resetPodTotals() {
document.querySelectorAll('[id^="podcpu-"]').forEach(el => {
const ps = String(el.getAttribute('data-podstatus') || '').toLowerCase();
if (ps === 'inactive') {
el.textContent = '-';
el.classList.remove('stale');
} else {
el.textContent = '—';
el.classList.add('stale');
}
});
document.querySelectorAll('[id^="podmem-"]').forEach(el => {
const ps = String(el.getAttribute('data-podstatus') || '').toLowerCase();
if (ps === 'inactive') {
el.textContent = '-';
el.classList.remove('stale');
} else {
el.textContent = '—';
el.classList.add('stale');
}
});
}
async function containerInspect(name) {
try {
const res = await api(`/containers/inspect/${encodeURIComponent(name)}`, 'GET');
showModal(`Inspect: ${name}`, JSON.stringify(res, null, 2));
} catch (e) {
showModal(`Inspect fout: ${name}`, e.stack || e.message);
}
}
async function containerLogs(name) {
try {
const res = await api(`/containers/logs/${encodeURIComponent(name)}`, 'GET');
const logs = res.logs ?? JSON.stringify(res, null, 2);
showModal(`Logs: ${name}`, logs);
} catch (e) {
showModal(`Logs fout: ${name}`, e.stack || e.message);
}
}
async function containerAction(action, name) {
try {
const res = await api(`/containers/${encodeURIComponent(action)}/${encodeURIComponent(name)}`, 'POST');
showModal(`Container ${action}: ${name}`, JSON.stringify(res, null, 2));
await fetchContainers();
} catch (e) {
showModal(`Container ${action} fout`, e.stack || e.message);
}
}
function cssSafeId(s) {
const bytes = new TextEncoder().encode(String(s));
let bin = '';
bytes.forEach(b => bin += String.fromCharCode(b));
return btoa(bin).replaceAll('=','').replaceAll('+','-').replaceAll('/','_');
}
function normalizeContainerName(n) {
if (!n) return '';
// Podman kan soms "/name" geven, we strippen de leading slash
return ('' + n).replace(/^\//, '');
}
function formatBytes(b) {
const n = Number(b || 0);
if (n >= 1024**3) return (n / 1024**3).toFixed(1) + "GiB";
if (n >= 1024**2) return (n / 1024**2).toFixed(0) + "MiB";
if (n >= 1024) return (n / 1024).toFixed(0) + "KiB";
return n + "B";
}