feat(containers-dashboard): groeperen containers toegevoegd
This commit is contained in:
+127
-30
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user