Files
podman-mvp/webui/html/assets/js/tabs/images.js
T
kodi 3a80ba09af feat(ui): ondersteuning Containerfile zichtbaar in build UI
- Label aangepast naar 'Dockerfile/Containerfile'
- Picker-titel aangepast naar 'Kies Dockerfile/Containerfile'
- Default waarde van buildDockerfile input leeggemaakt
- Validatiemelding aangepast naar 'Dockerfile/Containerfile'

Geen backend- of API-wijzigingen; dockerfile blijft leidend veld.
2026-03-01 11:09:18 +01:00

404 lines
11 KiB
JavaScript

let imagesData = [];
let imagesSort = { field: null, dir: null };
async function loadImages() {
const res = await fetch("/api/images");
const images = await res.json();
imagesData = images;
updateSortIndicators();
applyImageSorting();
}
function renderImages(images) {
const tbody = document.getElementById("images-tbody");
tbody.innerHTML = "";
images.forEach(img => {
const tr = document.createElement("tr");
const repoTag = (img.RepoTags && img.RepoTags.length > 0)
? img.RepoTags[0]
: "<none>";
const shortId = img.Id.substring(0, 12);
const sizeMB = (img.Size / 1024 / 1024).toFixed(1);
const created = img.Created ? new Date(img.Created * 1000).toLocaleString() : "-";
const containers = img.Containers || 0;
const fullId = img.Id;
const status = containers > 0
? `<span class="badge badge-green">In use</span>`
: `<span class="badge badge-yellow">Unused</span>`;
const disabled = containers > 0 ? "disabled" : "";
tr.innerHTML = `
<td>
<input type="checkbox" class="image-checkbox" value="${fullId}" ${disabled}>
</td>
<td>${repoTag}</td>
<td>${shortId}</td>
<td>${sizeMB} MB</td>
<td>${created}</td>
<td>${containers}</td>
<td>${status}</td>
<td>
<button class="btn small bad" onclick="removeSingleImage('${fullId}')" ${disabled}>
Remove
</button>
</td>
`;
tbody.appendChild(tr);
});
}
function toggleSelectAllImages(master) {
document.querySelectorAll(".image-checkbox:not(:disabled)")
.forEach(cb => cb.checked = master.checked);
}
async function removeSingleImage(id) {
if (!confirm("Image verwijderen?")) return;
await fetch("/api/images/" + encodeURIComponent(id), {
method: "DELETE"
});
await loadImages();
}
async function removeSelectedImages() {
const selected = Array.from(document.querySelectorAll(".image-checkbox:checked"))
.map(cb => cb.value);
if (!selected.length) {
alert("Geen images geselecteerd.");
return;
}
if (!confirm("Geselecteerde images verwijderen?")) return;
await fetch("/api/images/remove", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ images: selected })
});
await loadImages();
}
async function pruneUnusedImages() {
if (!confirm("Alle unused images verwijderen?")) return;
await fetch("/api/images/prune?all=true", {
method: "POST"
});
await loadImages();
}
// ---------- Build Modal ----------
function openBuildModal() {
document.getElementById("buildModalBack").style.display = "flex";
document.getElementById("buildOutput").value = "";
const ctxEl = document.getElementById("buildContext");
const tagEl = document.getElementById("buildTag");
// Reset auto-mode
tagEl.dataset.auto = "1";
// Update tag whenever context changes
ctxEl.oninput = () => {
if (tagEl.dataset.auto === "1") {
const suggestion = suggestTagFromContext(ctxEl.value);
tagEl.value = suggestion;
}
};
// If user types manually → stop auto mode
tagEl.oninput = () => {
tagEl.dataset.auto = "0";
};
}
function hideBuildModal() {
document.getElementById("buildModalBack").style.display = "none";
}
function closeBuildModal(e) {
if (e.target.id === "buildModalBack") hideBuildModal();
}
async function buildImage() {
const context = document.getElementById("buildContext").value.trim();
const dockerfile = document.getElementById("buildDockerfile").value.trim();
const tag = document.getElementById("buildTag").value.trim();
const pull = document.getElementById("buildPull").checked;
const nocache = document.getElementById("buildNoCache").checked;
const outputBox = document.getElementById("buildOutput");
if (!context || !dockerfile || !tag) {
alert("Vul context_dir, Dockerfile/Containerfile en tag in.");
return;
}
if (!ensureSystemdContextOrAlert(context)) {
return;
}
outputBox.value = "Starting build...\n";
try {
const res = await fetch("/api/images/build", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
context_dir: context,
dockerfile: dockerfile,
tag: tag,
pull: pull,
nocache: nocache
})
});
const ct = (res.headers.get("content-type") || "").toLowerCase();
let data;
if (ct.includes("application/json")) {
data = await res.json();
} else {
const text = await res.text();
data = { ok: res.ok, status: res.status, non_json: true, body: text.slice(0, 4000) };
}
if (!res.ok) {
outputBox.value += "\nERROR:\n" + JSON.stringify(data, null, 2);
return;
}
outputBox.value += data.output || "Build completed.";
await loadImages();
} catch (err) {
outputBox.value += "\nERROR:\n" + err.message;
}
}
// ---------- Dockerfile picker ----------
function openDockerfilePicker() {
document.getElementById("dfPickerBack").style.display = "flex";
const search = document.getElementById("dfPickerSearch");
if (search) {
search.value = "";
search.oninput = () => renderDockerfilePickerList(window.__dfPickerAll || []);
}
refreshDockerfilePicker();
}
function hideDockerfilePicker() {
document.getElementById("dfPickerBack").style.display = "none";
}
function closeDockerfilePicker(e) {
if (e.target.id === "dfPickerBack") hideDockerfilePicker();
}
async function refreshDockerfilePicker() {
const listEl = document.getElementById("dfPickerList");
listEl.textContent = "Laden...";
try {
const res = await fetch("/api/files/tree");
const tree = await res.json(); // [{path:"systemd/..", files:[...]}]
const candidates = [];
for (const folder of (tree || [])) {
const folderPath = (folder.path || "").replace(/^\/+/, ""); // e.g. systemd/foo
if (!folderPath || !(folderPath === "systemd" || folderPath.startsWith("systemd/"))) continue;
const files = folder.files || [];
for (const f of files) {
if (!isDockerfileName(f)) continue;
// full path under workloads-root (without leading slash)
const full = folderPath === "systemd" ? `systemd/${f}` : `${folderPath}/${f}`;
candidates.push(full);
}
}
// sort nice
candidates.sort((a, b) => a.localeCompare(b));
window.__dfPickerAll = candidates;
renderDockerfilePickerList(candidates);
} catch (e) {
listEl.textContent = "Fout bij laden: " + (e.message || e);
}
}
function isDockerfileName(name) {
const n = String(name || "").toLowerCase();
if (n === "dockerfile" || n === "containerfile") return true;
if (n.endsWith(".dockerfile") || n.endsWith(".containerfile")) return true;
return false;
}
function renderDockerfilePickerList(all) {
const listEl = document.getElementById("dfPickerList");
const q = (document.getElementById("dfPickerSearch")?.value || "").trim().toLowerCase();
const filtered = (all || []).filter(p => !q || p.toLowerCase().includes(q));
if (!filtered.length) {
listEl.innerHTML = `<div class="muted">Geen matches.</div>`;
return;
}
// Render as clickable buttons
listEl.innerHTML = filtered.map(p => {
const safe = p.replace(/"/g, "&quot;");
return `
<div style="display:flex; align-items:center; justify-content:space-between; gap:10px; padding:6px 0; border-bottom:1px dashed rgba(36,52,95,.35);">
<span>${safe}</span>
<button class="btn small ok" type="button" onclick="chooseDockerfilePath('${encodeURIComponent(p)}')">Kies</button>
</div>
`;
}).join("");
}
function chooseDockerfilePath(encodedPath) {
const fullPath = decodeURIComponent(encodedPath);
const idx = fullPath.lastIndexOf("/");
const contextDir = idx > 0 ? fullPath.substring(0, idx) : "systemd";
const dockerfile = idx > 0 ? fullPath.substring(idx + 1) : fullPath;
const ctxEl = document.getElementById("buildContext");
const tagEl = document.getElementById("buildTag");
ctxEl.value = contextDir;
document.getElementById("buildDockerfile").value = dockerfile;
if (tagEl.dataset.auto !== "0") {
const suggestion = suggestTagFromContext(contextDir);
tagEl.value = suggestion;
tagEl.dataset.auto = "1";
}
hideDockerfilePicker();
}
// ---------- Build helpers (4.3c) ----------
function suggestTagFromContext(contextDir) {
const p = String(contextDir || "").trim().replace(/^\/+/, "");
if (!p.startsWith("systemd/")) return "";
const parts = p.split("/").filter(Boolean);
// Als alleen "systemd" of "systemd/" → geen geldige image naam
if (parts.length <= 1) return "";
const name = parts[parts.length - 1];
const safe = name
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
return safe ? `localhost/${safe}:latest` : "";
}
function ensureSystemdContextOrAlert(contextDir) {
const p = String(contextDir || "").trim().replace(/^\/+/, "");
if (!p.startsWith("systemd/")) {
alert("Context directory moet beginnen met: systemd/\nVoorbeeld: systemd/buildtests/hello");
return false;
}
return true;
}
function sortImages(field) {
if (imagesSort.field !== field) {
imagesSort.field = field;
imagesSort.dir = "asc";
} else if (imagesSort.dir === "asc") {
imagesSort.dir = "desc";
} else if (imagesSort.dir === "desc") {
imagesSort.field = null;
imagesSort.dir = null;
} else {
imagesSort.dir = "asc";
}
updateSortIndicators();
applyImageSorting();
}
function applyImageSorting() {
let data = [...imagesData];
if (imagesSort.field && imagesSort.dir) {
data.sort((a, b) => {
let va, vb;
switch (imagesSort.field) {
case "repo":
va = (a.RepoTags && a.RepoTags[0]) || "";
vb = (b.RepoTags && b.RepoTags[0]) || "";
break;
case "id":
va = a.Id || "";
vb = b.Id || "";
break;
case "size":
va = a.Size || 0;
vb = b.Size || 0;
break;
case "created":
va = a.Created || 0;
vb = b.Created || 0;
break;
case "containers":
va = a.Containers || 0;
vb = b.Containers || 0;
break;
}
if (typeof va === "string") {
return imagesSort.dir === "asc"
? va.localeCompare(vb)
: vb.localeCompare(va);
} else {
return imagesSort.dir === "asc"
? va - vb
: vb - va;
}
});
}
renderImages(data);
}
function updateSortIndicators() {
// default: toon dat alles sorteerbaar is
document.querySelectorAll(".sort-indicator").forEach(el => el.textContent = "↕");
// als er geen sort actief is: laat defaults staan
if (!imagesSort.field || !imagesSort.dir) return;
// actieve kolom: ▲ of ▼
const el = document.getElementById("sort-" + imagesSort.field);
if (el) {
el.textContent = imagesSort.dir === "asc" ? "▲" : "▼";
}
}