/** * javascript/compose.js * * Compose panel: project list + an editable YAML view + save/up/down/pull/ * delete, backed by ajax/compose.php. See that file's header comment — * this is the one panel whose backend shells out to the `podman compose` * CLI, because no REST equivalent for Compose exists in libpod. */ (function () { 'use strict'; const P = window.Podman; let projects = []; let selected = null; const STARTER_YAML = 'services:\n' + ' app:\n' + ' image: docker.io/library/nginx:alpine\n' + ' ports:\n' + ' - "8080:80"\n'; function statusChip(status) { const cls = status === 'up' ? 'podman-chip-good' : (status === 'down' ? 'podman-chip-neutral' : 'podman-chip-warn'); return '' + P.escapeHtml(status) + ''; } function renderSidebar() { P.el('compose-sidebar').innerHTML = projects.map(function (p) { return '
' + '
' + P.escapeHtml(p.name) + ' ' + statusChip(p.status) + '
' + '
' + P.escapeHtml(p.path) + '
' + '
'; }).join('') || '
No compose projects yet — click "+ New Project".
'; } // Up/Down/Pull/Save/Delete all need an actual selected project to act on // — disabled (rather than left clickable and erroring) whenever nothing // is selected, e.g. right after deleting the last project. function setToolbarEnabled(enabled) { ['compose-action-up', 'compose-action-down', 'compose-action-pull', 'compose-action-save', 'compose-action-delete'].forEach(function (id) { P.el(id).disabled = !enabled; }); P.el('compose-yaml').disabled = !enabled; } function loadYaml(name) { P.el('compose-title').textContent = name + ' / compose.yaml'; P.el('compose-yaml').value = 'Loading…'; return P.get('compose', 'get', { project: name }).then(function (data) { P.el('compose-yaml').value = data.yaml; }).catch(function (err) { P.el('compose-yaml').value = 'Error: ' + err.message; }); } function selectProject(name) { selected = name; setToolbarEnabled(true); renderSidebar(); loadYaml(name); } function loadProjects() { return P.get('compose', 'list').then(function (data) { projects = data; if (selected && !projects.some(function (p) { return p.name === selected; })) { selected = null; } if (!selected && projects.length > 0) selected = projects[0].name; renderSidebar(); if (selected) { setToolbarEnabled(true); loadYaml(selected); } else { setToolbarEnabled(false); P.el('compose-title').textContent = '—'; P.el('compose-yaml').value = ''; } }).catch(function (err) { P.el('compose-sidebar').innerHTML = '
' + P.escapeHtml(err.message) + '
'; }); } function runAction(action) { if (!selected) return; const btn = P.el('compose-action-' + action); btn.disabled = true; // `podman compose` output can run to many lines — a log modal (same // pattern as Settings' service Start/Stop/Restart) fits that; a toast // has to stay short and auto-dismiss. const modal = P.openLogModal('podman compose ' + action + ' — ' + selected); P.post('compose', action, { project: selected }).then(function (data) { (data.output || 'Done.').split('\n').forEach(function (line) { modal.log(line); }); modal.done(); return loadProjects(); }).catch(function (err) { modal.log('Failed: ' + err.message); modal.done('Close'); }).finally(function () { btn.disabled = false; }); } function saveYaml() { if (!selected) return; const btn = P.el('compose-action-save'); btn.disabled = true; P.post('compose', 'save', { project: selected, yaml: P.el('compose-yaml').value }).then(function () { P.toast('Saved ' + selected + '.', 'success'); return loadProjects(); }).catch(function (err) { P.toast('Save failed: ' + err.message, 'error'); }).finally(function () { btn.disabled = false; }); } function deleteProject() { if (!selected) return; P.confirm('Delete project "' + selected + '"? This stops it (if running) and permanently removes its compose.yaml.', { danger: true, confirmLabel: 'Delete' }).then(function (ok) { if (!ok) return; const btn = P.el('compose-action-delete'); btn.disabled = true; const name = selected; P.post('compose', 'remove', { project: selected }).then(function () { selected = null; P.toast('Deleted ' + name + '.', 'success'); return loadProjects(); }).catch(function (err) { P.toast('Delete failed: ' + err.message, 'error'); btn.disabled = false; }); }); } function openNewProjectModal() { P.openFormModal({ title: 'New Compose Project', submitLabel: 'Create', fields: [ { name: 'name', label: 'Project name', required: true, placeholder: 'my-stack', hint: 'Letters, digits, "_", "-" only — no spaces.' }, ], onSubmit: function (values) { if (!/^[a-zA-Z0-9_-]+$/.test(values.name)) { return Promise.reject(new Error('Project name can only contain letters, digits, "_", "-" — no spaces.')); } return P.post('compose', 'save', { project: values.name, yaml: STARTER_YAML }).then(function () { selected = values.name; return loadProjects(); }); }, }); } function init() { P.el('compose-sidebar').addEventListener('click', function (e) { const item = e.target.closest('.podman-compose-proj[data-name]'); if (item) selectProject(item.dataset.name); }); P.el('compose-new-btn').addEventListener('click', openNewProjectModal); P.el('compose-action-up').addEventListener('click', function () { runAction('up'); }); P.el('compose-action-down').addEventListener('click', function () { runAction('down'); }); P.el('compose-action-pull').addEventListener('click', function () { runAction('pull'); }); P.el('compose-action-save').addEventListener('click', saveYaml); P.el('compose-action-delete').addEventListener('click', deleteProject); setToolbarEnabled(false); return loadProjects(); } P.registerPanel('compose', { init: init, refresh: loadProjects }); })();