/** * javascript/images.js * * Images panel: table + a "Pull Image" action, backed by ajax/images.php. */ (function () { 'use strict'; const P = window.Podman; let images = []; function rowHtml(img) { const created = img.createdAt ? P.formatRelativeTime(img.createdAt) : '—'; return '' + '' + '' + P.escapeHtml(img.repository) + '' + '' + P.escapeHtml(img.tag) + '' + '' + P.escapeHtml(img.shortId) + '' + '' + P.escapeHtml(img.sizeFormatted) + '' + '' + created + '' + '' + img.usedBy + '' + '
' + '' + '
' + ''; } function render() { const tbody = P.el('images-tbody'); tbody.innerHTML = images.length ? images.map(rowHtml).join('') : 'No images.'; } function load() { const tbody = P.el('images-tbody'); tbody.innerHTML = P.loadingRow(7); return P.get('images', 'list').then(function (data) { images = data; render(); }).catch(function (err) { tbody.innerHTML = P.errorRow(7, err.message); }); } function init() { P.el('images-pull-btn').addEventListener('click', function () { P.openFormModal({ title: 'Pull Image', submitLabel: 'Pull', fields: [ { name: 'reference', label: 'Image reference', required: true, placeholder: 'docker.io/library/postgres:16' }, ], onSubmit: function (values) { return P.post('images', 'pull', { reference: values.reference }).then(load); }, }); }); P.el('images-prune-btn').addEventListener('click', function () { // Computed client-side from the list already on screen — no extra // round trip needed, and it lets the confirm() be specific instead // of a generic warning. "Unused" here matches libpod's own // definition (zero containers, running or stopped, referencing the // image) — the same "Used By" count already shown in the table, not // just dangling/untagged images. Found live that this can be far // more aggressive than expected: with no containers at all, it // removes every image on the host. const unused = images.filter(function (img) { return img.usedBy === 0; }); if (!unused.length) { P.toast('No unused images to remove — every image is referenced by at least one container.', 'info'); return; } const totalBytes = unused.reduce(function (sum, img) { return sum + img.sizeBytes; }, 0); const btn = this; P.confirm( 'Remove ' + unused.length + ' image(s) not used by any container (' + P.formatBytes(totalBytes) + ')? ' + 'This removes any tagged image with zero containers using it, not just dangling ones.', { danger: true, confirmLabel: 'Remove' } ).then(function (ok) { if (!ok) return; btn.disabled = true; P.post('images', 'prune').then(function (result) { btn.disabled = false; P.toast('Removed ' + result.removedCount + ' image(s), reclaimed ' + P.formatBytes(result.reclaimedBytes) + '.', 'success'); return load(); }).catch(function (err) { btn.disabled = false; P.toast('Prune failed: ' + err.message, 'error'); }); }); }); P.el('images-tbody').addEventListener('click', function (e) { const btn = e.target.closest('button[data-action]'); if (!btn || btn.disabled) return; const id = btn.closest('tr').dataset.id; if (btn.dataset.action === 'tag') { P.openFormModal({ title: 'Add Tag', submitLabel: 'Add tag', fields: [ { name: 'repo', label: 'Repository', required: true, placeholder: 'my-registry.local/my-image' }, { name: 'tag', label: 'Tag', placeholder: 'latest' }, ], onSubmit: function (values) { return P.post('images', 'tag', { id: id, repo: values.repo, tag: values.tag || 'latest' }).then(load); }, }); return; } if (btn.dataset.action === 'remove') { P.confirm('Remove this image?', { danger: true, confirmLabel: 'Remove' }).then(function (ok) { if (!ok) return; btn.disabled = true; P.post('images', 'remove', { id: id }).then(load).catch(function (err) { P.toast('Remove failed: ' + err.message, 'error'); btn.disabled = false; }); }); } }); return load(); } P.registerPanel('images', { init: init, refresh: load }); })();