feat(containers-dashboard): groeperen containers toegevoegd
This commit is contained in:
+127
-30
@@ -220,6 +220,14 @@
|
|||||||
overflow:auto;
|
overflow:auto;
|
||||||
}
|
}
|
||||||
.hint{font-size:12px;color:var(--muted);margin-top:8px;line-height:1.35}
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -623,42 +631,131 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---- Containers ----
|
// ---- 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() {
|
async function fetchContainers() {
|
||||||
const containers = await api('/containers-dashboard', 'GET');
|
const containers = await api('/containers-dashboard', 'GET');
|
||||||
const list = Array.isArray(containers) ? containers : (containers?.containers || []);
|
const list = Array.isArray(containers) ? containers : (containers?.containers || []);
|
||||||
document.getElementById('countContainers').textContent = list.length;
|
document.getElementById('countContainers').textContent = list.length;
|
||||||
|
|
||||||
const tbody = document.getElementById('containersTbody');
|
const tbody = document.getElementById('containersTbody');
|
||||||
tbody.innerHTML = list.map(c => {
|
renderContainersGrouped(list, tbody);
|
||||||
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('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let containersStatsES = null;
|
let containersStatsES = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user