diff --git a/webui/html/assets/js/tabs/containers.js b/webui/html/assets/js/tabs/containers.js new file mode 100644 index 0000000..81f7852 --- /dev/null +++ b/webui/html/assets/js/tabs/containers.js @@ -0,0 +1,394 @@ +// ---- 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 ` + + + + + `; +} + +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 ` + + ${esc(name)} + ${badgeFromStatus(status)} + ${podName} + ${esc(image)} + ${badgeFromStatus(managed)} + - + - + ${ports || '-'} + +
+ + + ${renderActionsDropdown(menuId, 'containerAction', esc(name))} +
+ + + `; +} + +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 += ` + + + + ${collapsed ? 'â–ļ' : 'â–ŧ'} ${esc(pod)} + + + + + + ${label} + + + + - + - + ${isRealPod ? 'pod' : '-'} + + - + - + ${esc(podPortsText)} + + ${isRealPod ? ` +
+ + + + + ${renderActionsDropdown(podMenuId, 'podAction', esc(pod))} +
+ ` : '-'} + + + `; + + 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( + '