Add GPU/macvlan passthrough, container edit/update, image prune/tag
Create Container form: - GPU passthrough dropdown (AMD/Intel via /dev/dri detection, NVIDIA excluded since it needs a different runtime) - device paths strictly validated server-side against the host's own detected list. - Macvlan network support: selecting a macvlan network reveals a static IP field and hides port mappings (meaningless once the container has its own LAN address), matching Unraid Docker Manager's "Custom: br0" behavior. Networks panel gained a matching macvlan network-creation flow, with the parent-interface dropdown read from Unraid's own network.cfg so it lists exactly what Docker Manager itself offers. Containers panel: - Edit: reopens the create form pre-filled from the container's current config (image/ports/volumes/env/network/restart policy/GPU/static IP); saving stops+removes the old container and recreates it under the same settings, since podman/Docker have no in-place "modify" API for most of this. - Update: same stop/remove/recreate flow, but pulls the current image first. "Check for Updates" compares each in-use image's local digest against its origin registry (Docker Hub/GHCR/self-hosted registries all verified live) with no podman-side feature backing it - implemented via the registry's own HTTP API. A small log-modal shows progress for both actions instead of a silent wait. - Fixed a real bug hit live: PodmanClient's flat 15s HTTP timeout aborted real image pulls/container creates mid-request; bumped to 600s (nginx already allows up to 640s for this plugin's requests). Images panel: - "Prune unused" (removes every image with zero containers referencing it, not just dangling ones - confirmation copy says so explicitly since this is more aggressive than it sounds) and per-image "Tag". Also several real UI bugs found via live screenshots: unused-image prune having no visible effect until reloaded, table action-button columns drifting row to row (a bare "display:flex" on a <td> was fighting the table layout algorithm), Templates category badges dumping raw multi-tag strings from real Unraid templates, and low-contrast search/filter controls that were nearly invisible against the card background. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,21 +43,120 @@
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
P.el('networks-create-btn').addEventListener('click', function () {
|
||||
P.openFormModal({
|
||||
title: 'New Network',
|
||||
submitLabel: 'Create',
|
||||
fields: [
|
||||
{ name: 'name', label: 'Network name', required: true, placeholder: 'my-network' },
|
||||
{ name: 'subnet', label: 'Subnet (optional)', placeholder: '10.89.2.0/24' },
|
||||
],
|
||||
onSubmit: function (values) {
|
||||
return P.post('networks', 'create', { name: values.name, driver: 'bridge', subnet: values.subnet || undefined }).then(load);
|
||||
},
|
||||
});
|
||||
// Purpose-built modal (not app.js's generic openFormModal, which only
|
||||
// supports flat always-visible text fields) — the parent-interface
|
||||
// dropdown and gateway field only make sense for "macvlan" and need to
|
||||
// show/hide based on the driver choice.
|
||||
function openCreateNetworkModal() {
|
||||
const backdrop = document.createElement('div');
|
||||
backdrop.className = 'podman-modal-backdrop';
|
||||
backdrop.innerHTML = '' +
|
||||
'<div class="podman-modal" role="dialog" aria-modal="true">' +
|
||||
'<div class="podman-modal-head"><h3>New Network</h3></div>' +
|
||||
'<form class="podman-modal-body">' +
|
||||
'<div class="podman-modal-field"><label>Network name</label>' +
|
||||
'<input type="text" id="cn-name" placeholder="my-network"></div>' +
|
||||
'<div class="podman-modal-field"><label>Type</label>' +
|
||||
'<select id="cn-driver">' +
|
||||
'<option value="bridge">Bridge (isolated, NAT — default)</option>' +
|
||||
'<option value="macvlan">Macvlan (containers get a real IP on your LAN)</option>' +
|
||||
'</select></div>' +
|
||||
'<div class="podman-modal-field" id="cn-parent-field" style="display:none;">' +
|
||||
'<label>Parent interface</label><select id="cn-parent"></select>' +
|
||||
'<div class="hint">Same interface Docker Manager\'s "Custom: br0"-style networks use.</div></div>' +
|
||||
'<div class="podman-modal-field"><label id="cn-subnet-label">Subnet (optional)</label>' +
|
||||
'<input type="text" class="mono" id="cn-subnet" placeholder="10.89.2.0/24"></div>' +
|
||||
'<div class="podman-modal-field" id="cn-gateway-field" style="display:none;">' +
|
||||
'<label>Gateway</label><input type="text" class="mono" id="cn-gateway" placeholder="10.1.1.1"></div>' +
|
||||
'</form>' +
|
||||
'<div class="podman-modal-actions">' +
|
||||
'<button type="button" class="podman-btn podman-btn-ghost" data-role="cancel">Cancel</button>' +
|
||||
'<button type="button" class="podman-btn podman-btn-primary" data-role="submit">Create</button>' +
|
||||
'</div></div>';
|
||||
|
||||
(document.querySelector('.podman-plugin') || document.body).appendChild(backdrop);
|
||||
|
||||
let parentInterfaces = [];
|
||||
P.get('networks', 'list_parent_interfaces').then(function (interfaces) {
|
||||
parentInterfaces = interfaces;
|
||||
const select = backdrop.querySelector('#cn-parent');
|
||||
select.innerHTML = interfaces.map(function (i) {
|
||||
return '<option value="' + P.escapeHtml(i.interface) + '">' + P.escapeHtml(i.label) + '</option>';
|
||||
}).join('');
|
||||
}).catch(function () { /* macvlan option just won't have anything to pick if this fails */ });
|
||||
|
||||
backdrop.querySelector('#cn-driver').addEventListener('change', function (e) {
|
||||
const isMacvlan = e.target.value === 'macvlan';
|
||||
backdrop.querySelector('#cn-parent-field').style.display = isMacvlan ? '' : 'none';
|
||||
backdrop.querySelector('#cn-gateway-field').style.display = isMacvlan ? '' : 'none';
|
||||
backdrop.querySelector('#cn-subnet-label').textContent = isMacvlan ? 'Subnet' : 'Subnet (optional)';
|
||||
});
|
||||
|
||||
backdrop.querySelector('#cn-name').focus();
|
||||
|
||||
function close() { backdrop.remove(); }
|
||||
|
||||
function showError(message) {
|
||||
let box = backdrop.querySelector('.podman-modal-error');
|
||||
if (!box) {
|
||||
box = document.createElement('div');
|
||||
box.className = 'podman-modal-error';
|
||||
backdrop.querySelector('.podman-modal-body').appendChild(box);
|
||||
}
|
||||
box.textContent = message;
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const name = backdrop.querySelector('#cn-name').value.trim();
|
||||
if (!name) {
|
||||
showError('"Network name" is required.');
|
||||
return;
|
||||
}
|
||||
const driver = backdrop.querySelector('#cn-driver').value;
|
||||
const subnet = backdrop.querySelector('#cn-subnet').value.trim();
|
||||
const gateway = backdrop.querySelector('#cn-gateway').value.trim();
|
||||
const parentInterface = backdrop.querySelector('#cn-parent').value;
|
||||
|
||||
if (driver === 'macvlan') {
|
||||
if (!subnet) {
|
||||
showError('"Subnet" is required for a macvlan network.');
|
||||
return;
|
||||
}
|
||||
if (!parentInterfaces.length) {
|
||||
showError('No host bridge/VLAN interface available to attach to.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const submitBtn = backdrop.querySelector('[data-role="submit"]');
|
||||
submitBtn.disabled = true;
|
||||
P.post('networks', 'create', {
|
||||
name: name,
|
||||
driver: driver,
|
||||
subnet: subnet || undefined,
|
||||
gateway: gateway || undefined,
|
||||
parentInterface: driver === 'macvlan' ? parentInterface : undefined,
|
||||
}).then(function () {
|
||||
close();
|
||||
return load();
|
||||
}).catch(function (err) {
|
||||
submitBtn.disabled = false;
|
||||
showError(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
backdrop.querySelector('[data-role="cancel"]').addEventListener('click', close);
|
||||
backdrop.querySelector('[data-role="submit"]').addEventListener('click', submit);
|
||||
backdrop.querySelector('form').addEventListener('submit', function (e) { e.preventDefault(); submit(); });
|
||||
backdrop.addEventListener('click', function (e) { if (e.target === backdrop) close(); });
|
||||
document.addEventListener('keydown', function onKey(e) {
|
||||
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', onKey); }
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
P.el('networks-create-btn').addEventListener('click', openCreateNetworkModal);
|
||||
|
||||
P.el('networks-tbody').addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('button[data-action="remove"]');
|
||||
if (!btn || btn.disabled) return;
|
||||
|
||||
Reference in New Issue
Block a user