Make Compose panel editable: create/edit/delete projects
The YAML view was read-only with no way to create a new project at all. Add save/remove AJAX actions (validated via a real `podman compose ... config` dry-run, written to a .new sibling and only renamed into place on success) and a New Project/Save/Delete UI backed by an editable textarea instead of a <pre>. Also strip ANSI escape codes from compose command output so podman's own provider banner doesn't show as literal garbage in error alerts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* javascript/compose.js
|
||||
*
|
||||
* Compose panel: project list + read-only YAML view + up/down/pull,
|
||||
* 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.
|
||||
* 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';
|
||||
@@ -12,6 +12,13 @@
|
||||
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 '<span class="podman-chip ' + cls + '"><span class="d"></span>' + P.escapeHtml(status) + '</span>';
|
||||
@@ -23,21 +30,32 @@
|
||||
'<div class="name" style="display:flex; justify-content:space-between; gap:8px;">' + P.escapeHtml(p.name) + ' ' + statusChip(p.status) + '</div>' +
|
||||
'<div class="path">' + P.escapeHtml(p.path) + '</div>' +
|
||||
'</div>';
|
||||
}).join('') || '<div class="podman-empty-note">No compose projects under /boot/config/plugins/podman/compose/</div>';
|
||||
}).join('') || '<div class="podman-empty-note">No compose projects yet — click "+ New Project".</div>';
|
||||
}
|
||||
|
||||
// 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').textContent = 'Loading…';
|
||||
P.el('compose-yaml').value = 'Loading…';
|
||||
return P.get('compose', 'get', { project: name }).then(function (data) {
|
||||
P.el('compose-yaml').textContent = data.yaml;
|
||||
P.el('compose-yaml').value = data.yaml;
|
||||
}).catch(function (err) {
|
||||
P.el('compose-yaml').textContent = 'Error: ' + err.message;
|
||||
P.el('compose-yaml').value = 'Error: ' + err.message;
|
||||
});
|
||||
}
|
||||
|
||||
function selectProject(name) {
|
||||
selected = name;
|
||||
setToolbarEnabled(true);
|
||||
renderSidebar();
|
||||
loadYaml(name);
|
||||
}
|
||||
@@ -45,9 +63,19 @@
|
||||
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) loadYaml(selected);
|
||||
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 = '<div class="podman-error" style="padding:14px;">' + P.escapeHtml(err.message) + '</div>';
|
||||
});
|
||||
@@ -67,15 +95,65 @@
|
||||
});
|
||||
}
|
||||
|
||||
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 () {
|
||||
return loadProjects();
|
||||
}).catch(function (err) {
|
||||
alert('Save failed: ' + err.message);
|
||||
}).finally(function () {
|
||||
btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function deleteProject() {
|
||||
if (!selected) return;
|
||||
if (!confirm('Delete project "' + selected + '"? This stops it (if running) and permanently removes its compose.yaml.')) return;
|
||||
const btn = P.el('compose-action-delete');
|
||||
btn.disabled = true;
|
||||
P.post('compose', 'remove', { project: selected }).then(function () {
|
||||
selected = null;
|
||||
return loadProjects();
|
||||
}).catch(function (err) {
|
||||
alert('Delete failed: ' + err.message);
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user