3a80ba09af
- 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.
404 lines
11 KiB
JavaScript
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, """);
|
|
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" ? "▲" : "▼";
|
|
}
|
|
}
|