Files
podman-mvp/webui/html/assets/js/tabs/volumes.js
T
kodi 5f6719464d fix (ui/volumes): herstel container-koppeling via inspect endpoint
containers-dashboard geeft Mounts als strings (destination paden).
Volledige mount-info (Type + Name) zit alleen in /containers/inspect/{name}.

Fix: voor containers met niet-lege Mounts parallel inspect ophalen,
daarna filteren op Type === "volume" voor named volume koppeling.

Getest: postgresdb_data → postgres-db, n8n_data → n8n.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 17:51:37 +01:00

220 lines
7.3 KiB
JavaScript

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 : [];
// containers-dashboard geeft Mounts als strings (destination paden).
// Volledige mount-info (Type + Name) zit alleen in de inspect endpoint.
// Haal inspect op voor alle containers met niet-lege Mounts, parallel.
const containerList = Array.isArray(containers) ? containers : [];
const withMounts = containerList.filter(c => (c.Mounts || []).length > 0);
const inspectResults = await Promise.all(
withMounts.map(c => {
const name = (c.Names && c.Names[0]) || "";
if (!name) return Promise.resolve(null);
return fetch("/api/containers/inspect/" + encodeURIComponent(name))
.then(r => r.ok ? r.json() : null)
.catch(() => null);
})
);
// Bouw volume → containers mapping: filter op Type === "volume"
volumeContainersMap = {};
for (let i = 0; i < withMounts.length; i++) {
const inspect = inspectResults[i];
if (!inspect) continue;
const cname = (withMounts[i].Names && withMounts[i].Names[0]) || "";
for (const m of (inspect.Mounts || [])) {
if (m.Type === "volume" && 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 = `<tr><td colspan="7">${box}</td></tr>`;
}
}
}
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, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
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 = `<tr><td colspan="7">${box}</td></tr>`;
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 =>
`<span class="badge muted" title="${_volEsc(k + "=" + labels[k])}">${_volEsc(k)}</span>`
).join(" ")
: `<span class="muted">-</span>`;
const containersHtml = cNames.length
? cNames.map(n => `<span class="badge ok">${_volEsc(n)}</span>`).join(" ")
: `<span class="muted">-</span>`;
const nameEnc = encodeURIComponent(name);
const disabledAttr = inUse ? `disabled title="In gebruik door een container"` : "";
const tr = document.createElement("tr");
tr.innerHTML = `
<td><strong>${_volEsc(name)}</strong></td>
<td class="muted">${_volEsc(driver)}</td>
<td class="muted mono" title="${_volEsc(mp)}">${_volEsc(mpShort)}</td>
<td class="muted">${created}</td>
<td>${labelHtml}</td>
<td>${containersHtml}</td>
<td>
<button class="btn small bad" onclick="removeVolume(decodeURIComponent('${nameEnc}'))" ${disabledAttr}>
Verwijder
</button>
</td>
`;
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}`);
}
}