feat(containers-dashboard): groeperen containers toegevoegd

This commit is contained in:
kodi
2026-02-18 15:46:31 +01:00
parent eecf4ad9f2
commit 35e5682b91
+127 -30
View File
@@ -220,6 +220,14 @@
overflow:auto;
}
.hint{font-size:12px;color:var(--muted);margin-top:8px;line-height:1.35}
.pod-group-row td {
background: rgba(255,255,255,0.04);
border-top: 1px solid rgba(255,255,255,0.08);
font-weight: 600;
}
.pod-group-row:hover td {
background: rgba(255,255,255,0.07);
}
</style>
</head>
@@ -623,42 +631,131 @@
}
// ---- 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 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 ports = (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 small" onclick="containerInspect('${esc(name)}')">Inspect</button>
<button class="btn small" onclick="containerLogs('${esc(name)}')">Logs</button>
<button class="btn small ok" onclick="containerAction('start','${esc(name)}')">Start</button>
<button class="btn small warn" onclick="containerAction('restart','${esc(name)}')">Restart</button>
<button class="btn small bad" onclick="containerAction('stop','${esc(name)}')">Stop</button>
</div>
</td>
</tr>
`;
}
function renderContainersGrouped(list, tbody) {
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);
}
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);
});
const table = tbody.closest('table');
const colCount = table ? table.querySelectorAll('thead th').length : 9;
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);
html += `
<tr class="pod-group-row">
<td colspan="${colCount}">
<span class="pod-toggle" data-pod="${pod}" style="cursor:pointer; user-select:none;">
${collapsed ? '▶' : '▼'} <b>${esc(pod)}</b> <span style="opacity:.7;">(${items.length})</span>
</span>
</td>
</tr>
`;
for (const c of items) {
const row = renderContainerRow(c).replace(
'<tr',
`<tr class="pod-item-row" data-pod="${esc(pod)}"${collapsed ? ' style="display:none;"' : ''}`
);
html += row;
}
}
tbody.innerHTML = html;
tbody.onclick = (ev) => {
const t = ev.target.closest('.pod-toggle');
if (!t) return;
const pod = t.getAttribute('data-pod');
const isNowCollapsed = !_isCollapsed(pod);
_setCollapsed(pod, isNowCollapsed);
tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(pod)}"]`).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(pod)}"]`).length})</span>`;
};
}
async function fetchContainers() {
const containers = await api('/containers-dashboard', 'GET');
const list = Array.isArray(containers) ? containers : (containers?.containers || []);
document.getElementById('countContainers').textContent = list.length;
const tbody = document.getElementById('containersTbody');
tbody.innerHTML = list.map(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 ports = (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 small" onclick="containerInspect('${esc(name)}')">Inspect</button>
<button class="btn small" onclick="containerLogs('${esc(name)}')">Logs</button>
<button class="btn small ok" onclick="containerAction('start','${esc(name)}')">Start</button>
<button class="btn small warn" onclick="containerAction('restart','${esc(name)}')">Restart</button>
<button class="btn small bad" onclick="containerAction('stop','${esc(name)}')">Stop</button>
</div>
</td>
</tr>
`;
}).join('');
renderContainersGrouped(list, tbody);
}
let containersStatsES = null;