Terminal panel now opens a genuinely interactive shell (ttyd bound to a unix socket, proxied through Unraid's own /logterminal/ nginx location — the same mechanism Unraid's own Docker "Console" button uses) instead of one-shot exec calls, shown inline with a Disconnect action; bash is the default shell. Container/shell selectors and action buttons are now correctly bottom-aligned (root cause: Unraid's theme puts a 10px margin on every <button>, never reset before). Destructive actions (Disconnect, Compose/Template Delete, Volumes/Images/ Networks Remove) get a consistent, solid red treatment at rest instead of only tinting on hover, via new --bad-strong/--bad-contrast tokens. Settings panel restructured: a real save toolbar instead of a button buried in an empty-label row, card subtitles, a toggle switch instead of a bare checkbox, and installed-package versions shown as chips. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
177 lines
7.4 KiB
JavaScript
177 lines
7.4 KiB
JavaScript
/**
|
|
* javascript/networks.js
|
|
*
|
|
* Networks panel: table + create/remove, backed by ajax/networks.php.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
const P = window.Podman;
|
|
let networks = [];
|
|
|
|
function rowHtml(n) {
|
|
const nameCell = n.isDefault
|
|
? '<strong>' + P.escapeHtml(n.name) + '</strong> <span class="podman-row-sub">(default)</span>'
|
|
: P.escapeHtml(n.name);
|
|
const removeDisabled = n.isDefault || n.containers > 0;
|
|
return '' +
|
|
'<tr data-name="' + P.escapeHtml(n.name) + '">' +
|
|
'<td>' + nameCell + '</td>' +
|
|
'<td><span class="podman-chip podman-chip-neutral">' + P.escapeHtml(n.driver) + '</span></td>' +
|
|
'<td class="mono">' + P.escapeHtml(n.subnet || '—') + '</td>' +
|
|
'<td class="mono">' + P.escapeHtml(n.gateway || '—') + '</td>' +
|
|
'<td class="tnum">' + n.containers + '</td>' +
|
|
'<td class="podman-actions"><div class="podman-actions-row"><button class="podman-btn podman-btn-icon podman-btn-danger" data-action="remove"' +
|
|
(removeDisabled ? ' disabled' : '') + ' title="Remove">🗑</button></div></td>' +
|
|
'</tr>';
|
|
}
|
|
|
|
function render() {
|
|
const tbody = P.el('networks-tbody');
|
|
tbody.innerHTML = networks.length
|
|
? networks.map(rowHtml).join('')
|
|
: '<tr><td colspan="6" class="podman-empty-note">No networks.</td></tr>';
|
|
}
|
|
|
|
function load() {
|
|
const tbody = P.el('networks-tbody');
|
|
tbody.innerHTML = P.loadingRow(6);
|
|
return P.get('networks', 'list').then(function (data) {
|
|
networks = data;
|
|
render();
|
|
}).catch(function (err) {
|
|
tbody.innerHTML = P.errorRow(6, err.message);
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
const name = btn.closest('tr').dataset.name;
|
|
if (!confirm('Remove network "' + name + '"?')) return;
|
|
btn.disabled = true;
|
|
P.post('networks', 'remove', { name: name }).then(load).catch(function (err) {
|
|
alert('Remove failed: ' + err.message);
|
|
btn.disabled = false;
|
|
});
|
|
});
|
|
|
|
return load();
|
|
}
|
|
|
|
P.registerPanel('networks', { init: init, refresh: load });
|
|
})();
|