feat(ui): image tabblad uitgebreid met Dockerfile selectie en tag suggestie

This commit is contained in:
kodi
2026-02-21 14:09:13 +01:00
parent 815d16f872
commit d28633a22d
2 changed files with 301 additions and 0 deletions
+222
View File
@@ -17,6 +17,7 @@ function renderImages(images) {
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;
@@ -33,6 +34,7 @@ function renderImages(images) {
<td>${repoTag}</td>
<td>${shortId}</td>
<td>${sizeMB} MB</td>
<td>${created}</td>
<td>${containers}</td>
<td>${status}</td>
<td>
@@ -90,3 +92,223 @@ async function pruneUnusedImages() {
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 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 data = await res.json();
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;
}
+79
View File
@@ -153,6 +153,7 @@
<button class="btn" onclick="loadImages()">Ververs</button>
<button class="btn bad" onclick="removeSelectedImages()">Remove selected</button>
<button class="btn warn" onclick="pruneUnusedImages()">Prune unused</button>
<button class="btn ok" onclick="openBuildModal()">Build image</button>
</div>
</div>
<div class="cardBody">
@@ -165,6 +166,7 @@
<th>Repo / Tag</th>
<th>ID</th>
<th>Size</th>
<th>Created</th>
<th>Containers</th>
<th>Status</th>
<th style="width:100px;">Acties</th>
@@ -231,6 +233,83 @@
</div>
</div>
<!-- Build Modal -->
<div class="modalBack" id="buildModalBack" style="display:none;" onclick="closeBuildModal(event)">
<div class="modal" onclick="event.stopPropagation()" style="width:700px;">
<div class="modalHeader">
<div class="modalTitle">Build image</div>
<button class="btn small ghost" onclick="hideBuildModal()">Sluiten</button>
</div>
<div class="modalBody">
<div class="formRow">
<label>Context directory</label>
<input class="input" id="buildContext" placeholder="systemd/buildtests/hello">
</div>
<div class="formRow">
<label>Dockerfile</label>
<div class="row gap">
<input class="input" id="buildDockerfile" value="Dockerfile" style="flex:1;">
<button class="btn" type="button" onclick="openDockerfilePicker()">Kies...</button>
</div>
<div class="muted" style="margin-top:6px;">
Kiest een Dockerfile/Containerfile onder <span class="mono">systemd/</span>.
</div>
</div>
<div class="formRow">
<label>Tag</label>
<input class="input" id="buildTag" placeholder="localhost/myimage:latest">
</div>
<div class="formRow">
<label><input type="checkbox" id="buildPull"> Pull latest base image</label>
</div>
<div class="formRow">
<label><input type="checkbox" id="buildNoCache"> No cache</label>
</div>
<div style="margin-top:15px;">
<button class="btn ok" onclick="buildImage()">Start build</button>
</div>
<div style="margin-top:15px;">
<textarea id="buildOutput" class="textarea mono" style="height:200px;" readonly></textarea>
</div>
</div>
</div>
</div>
<!-- Dockerfile Picker Modal -->
<div class="modalBack" id="dfPickerBack" style="display:none;" onclick="closeDockerfilePicker(event)">
<div class="modal" onclick="event.stopPropagation()" style="width:760px;">
<div class="modalHeader">
<div class="modalTitle">Kies Dockerfile</div>
<button class="btn small ghost" onclick="hideDockerfilePicker()">Sluiten</button>
</div>
<div class="modalBody">
<div class="row gap" style="margin-bottom:10px;">
<input class="input" id="dfPickerSearch" placeholder="Zoek... (bijv. traefik, hello, Dockerfile)" style="flex:1;">
<button class="btn" type="button" onclick="refreshDockerfilePicker()">Ververs</button>
</div>
<div class="hint" style="margin-bottom:10px;">
Alleen bestanden onder <span class="mono">systemd/</span> die <span class="mono">Dockerfile</span>,
<span class="mono">Containerfile</span> of eindigen op <span class="mono">.dockerfile</span>/<span class="mono">.containerfile</span>.
</div>
<div class="input" style="padding:10px; max-height:360px; overflow:auto;">
<div id="dfPickerList" class="mono muted">Laden...</div>
</div>
</div>
</div>
</div>
<script>
let cmEditor = null;
// ---- API helper ----