From 249d24721c123df64caf981ed77fa1aa4f26c795 Mon Sep 17 00:00:00 2001 From: kodi Date: Mon, 23 Mar 2026 17:34:29 +0100 Subject: [PATCH] feat (ui): voeg Volumes tabblad toe aan webui Nieuw tabblad na Images met: - Tabel: Naam, Driver, Mountpoint (afgekapt + tooltip), Aangemaakt (relatieve tijd), Labels (pills), Containers (pills via Mounts koppeling) - Toolbar: Ververs, + Volume, Prune (met bevestigingsdialoog) - Verwijder knop per rij (uitgeschakeld als volume in gebruik) - Create Volume modal: naam (verplicht) + labels (key=value per regel) - Lege staat via renderStateBox volumes.js: _volEsc() voor XSS-safe rendering, encodeURIComponent voor onclick-handlers, parallel fetch volumes + containers-dashboard voor container-koppeling via Mounts[].Name. Co-Authored-By: Claude Sonnet 4.6 --- webui/html/assets/js/tabs/volumes.js | 202 +++++++++++++++++++++++++++ webui/html/index.html | 60 ++++++++ 2 files changed, 262 insertions(+) create mode 100644 webui/html/assets/js/tabs/volumes.js diff --git a/webui/html/assets/js/tabs/volumes.js b/webui/html/assets/js/tabs/volumes.js new file mode 100644 index 0000000..c7181ad --- /dev/null +++ b/webui/html/assets/js/tabs/volumes.js @@ -0,0 +1,202 @@ +let volumesData = []; +let volumeContainersMap = {}; + +async function loadVolumes() { + const tbody = document.getElementById("volumes-tbody"); + try { + const [volumes, containers] = await Promise.all([ + fetch("/api/volumes").then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }), + fetch("/api/containers-dashboard").then(r => r.ok ? r.json() : []).catch(() => []) + ]); + + volumesData = Array.isArray(volumes) ? volumes : []; + + // Bouw volume → containers mapping op basis van Mounts + volumeContainersMap = {}; + for (const c of (Array.isArray(containers) ? containers : [])) { + const cname = (c.Names && c.Names[0]) || ""; + for (const m of (c.Mounts || [])) { + if (m.Name) { + (volumeContainersMap[m.Name] = volumeContainersMap[m.Name] || []).push(cname); + } + } + } + + if (typeof window.updateNavCount === "function") { + window.updateNavCount("countNavVolumes", volumesData.length); + } + renderVolumes(volumesData); + } catch (e) { + volumesData = []; + if (typeof window.updateNavCount === "function") window.updateNavCount("countNavVolumes", 0); + if (tbody) { + const box = typeof window.renderStateBox === "function" + ? window.renderStateBox("error", "Volumes laden mislukt", e.message || String(e)) + : "Volumes laden mislukt."; + tbody.innerHTML = `${box}`; + } + } +} + +function _volRelTime(isoStr) { + if (!isoStr) return "-"; + const d = new Date(isoStr); + if (isNaN(d)) return String(isoStr); + const s = Math.floor((Date.now() - d.getTime()) / 1000); + if (s < 60) return `${s}s geleden`; + if (s < 3600) return `${Math.floor(s / 60)}m geleden`; + if (s < 86400) return `${Math.floor(s / 3600)}u geleden`; + return `${Math.floor(s / 86400)} dagen geleden`; +} + +function _volEsc(s) { + return String(s || "") + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(//g, ">"); +} + +function renderVolumes(volumes) { + const tbody = document.getElementById("volumes-tbody"); + if (!tbody) return; + tbody.innerHTML = ""; + + if (!volumes.length) { + const box = typeof window.renderStateBox === "function" + ? window.renderStateBox("empty", "Geen volumes", "Er zijn momenteel geen volumes gevonden.") + : "Geen volumes gevonden."; + tbody.innerHTML = `${box}`; + return; + } + + volumes.forEach(vol => { + const name = vol.Name || "-"; + const driver = vol.Driver || "-"; + const mp = vol.Mountpoint || ""; + const mpShort = mp.length > 45 ? mp.slice(0, 42) + "…" : mp; + const created = _volRelTime(vol.CreatedAt); + const labels = vol.Labels || {}; + const cNames = volumeContainersMap[name] || []; + const inUse = cNames.length > 0; + + const labelHtml = Object.keys(labels).length + ? Object.keys(labels).map(k => + `${_volEsc(k)}` + ).join(" ") + : `-`; + + const containersHtml = cNames.length + ? cNames.map(n => `${_volEsc(n)}`).join(" ") + : `-`; + + const nameEnc = encodeURIComponent(name); + const disabledAttr = inUse ? `disabled title="In gebruik door een container"` : ""; + + const tr = document.createElement("tr"); + tr.innerHTML = ` + ${_volEsc(name)} + ${_volEsc(driver)} + ${_volEsc(mpShort)} + ${created} + ${labelHtml} + ${containersHtml} + + + + `; + tbody.appendChild(tr); + }); +} + +async function removeVolume(name) { + if (!confirm(`Volume '${name}' verwijderen?\nDit kan niet ongedaan worden gemaakt.`)) return; + try { + const res = await fetch("/api/volumes/" + encodeURIComponent(name), { method: "DELETE" }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + alert(`Verwijderen mislukt (${res.status}): ${body}`); + return; + } + await loadVolumes(); + } catch (e) { + alert(`Fout: ${e.message}`); + } +} + +async function pruneVolumes() { + if (!confirm( + "Prune volumes\n\n" + + "Dit verwijdert alle volumes die niet aan een container gekoppeld zijn.\n" + + "Dit kan niet ongedaan worden gemaakt.\n\n" + + "Doorgaan?" + )) return; + try { + const res = await fetch("/api/volumes/prune", { method: "POST" }); + if (!res.ok) { + const body = await res.text().catch(() => ""); + alert(`Prune mislukt (${res.status}): ${body}`); + return; + } + const data = await res.json(); + const removed = Array.isArray(data) ? data.length : 0; + alert(`Prune voltooid. ${removed} volume(s) verwijderd.`); + await loadVolumes(); + } catch (e) { + alert(`Fout: ${e.message}`); + } +} + +// ---- Create Volume Modal ---- + +function openCreateVolumeModal() { + document.getElementById("createVolumeModalBack").style.display = "flex"; + document.getElementById("createVolumeName").value = ""; + document.getElementById("createVolumeLabels").value = ""; +} + +function hideCreateVolumeModal() { + document.getElementById("createVolumeModalBack").style.display = "none"; +} + +function closeCreateVolumeModal(e) { + if (e.target.id === "createVolumeModalBack") hideCreateVolumeModal(); +} + +async function createVolume() { + const name = document.getElementById("createVolumeName").value.trim(); + if (!name) { alert("Naam is verplicht."); return; } + + const labelsRaw = document.getElementById("createVolumeLabels").value.trim(); + const labels = {}; + if (labelsRaw) { + for (const line of labelsRaw.split(/\r?\n/)) { + const l = line.trim(); + if (!l) continue; + const idx = l.indexOf("="); + if (idx > 0) labels[l.slice(0, idx).trim()] = l.slice(idx + 1).trim(); + } + } + + const body = { name }; + if (Object.keys(labels).length) body.labels = labels; + + try { + const res = await fetch("/api/volumes", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body) + }); + if (!res.ok) { + const err = await res.text().catch(() => ""); + alert(`Aanmaken mislukt (${res.status}): ${err}`); + return; + } + hideCreateVolumeModal(); + await loadVolumes(); + } catch (e) { + alert(`Fout: ${e.message}`); + } +} diff --git a/webui/html/index.html b/webui/html/index.html index 1c8c9b4..96a530b 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -57,6 +57,9 @@
📦Images-
+
+ 🗄️Volumes- +
📁Files-
@@ -305,6 +308,35 @@ + +