feat(ui): image tabblad uitgebreid met Dockerfile selectie en tag suggestie
This commit is contained in:
@@ -17,6 +17,7 @@ function renderImages(images) {
|
|||||||
|
|
||||||
const shortId = img.Id.substring(0, 12);
|
const shortId = img.Id.substring(0, 12);
|
||||||
const sizeMB = (img.Size / 1024 / 1024).toFixed(1);
|
const sizeMB = (img.Size / 1024 / 1024).toFixed(1);
|
||||||
|
const created = img.Created ? new Date(img.Created * 1000).toLocaleString() : "-";
|
||||||
const containers = img.Containers || 0;
|
const containers = img.Containers || 0;
|
||||||
const fullId = img.Id;
|
const fullId = img.Id;
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ function renderImages(images) {
|
|||||||
<td>${repoTag}</td>
|
<td>${repoTag}</td>
|
||||||
<td>${shortId}</td>
|
<td>${shortId}</td>
|
||||||
<td>${sizeMB} MB</td>
|
<td>${sizeMB} MB</td>
|
||||||
|
<td>${created}</td>
|
||||||
<td>${containers}</td>
|
<td>${containers}</td>
|
||||||
<td>${status}</td>
|
<td>${status}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -89,4 +91,224 @@ async function pruneUnusedImages() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await loadImages();
|
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, """);
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
@@ -153,6 +153,7 @@
|
|||||||
<button class="btn" onclick="loadImages()">Ververs</button>
|
<button class="btn" onclick="loadImages()">Ververs</button>
|
||||||
<button class="btn bad" onclick="removeSelectedImages()">Remove selected</button>
|
<button class="btn bad" onclick="removeSelectedImages()">Remove selected</button>
|
||||||
<button class="btn warn" onclick="pruneUnusedImages()">Prune unused</button>
|
<button class="btn warn" onclick="pruneUnusedImages()">Prune unused</button>
|
||||||
|
<button class="btn ok" onclick="openBuildModal()">Build image</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cardBody">
|
<div class="cardBody">
|
||||||
@@ -165,6 +166,7 @@
|
|||||||
<th>Repo / Tag</th>
|
<th>Repo / Tag</th>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
|
<th>Created</th>
|
||||||
<th>Containers</th>
|
<th>Containers</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th style="width:100px;">Acties</th>
|
<th style="width:100px;">Acties</th>
|
||||||
@@ -231,6 +233,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
let cmEditor = null;
|
let cmEditor = null;
|
||||||
// ---- API helper ----
|
// ---- API helper ----
|
||||||
|
|||||||
Reference in New Issue
Block a user