Dashboard: - Plain-count stat tiles (Running/Stopped/Pods/Images/Volumes/Networks) separated from a single "Resource Usage" card (CPU/Memory/Swap/Storage meter rows) instead of forcing both into one tile grid, which produced awkward spanning-tile/dead-cell layouts. - Fixed CPU usage never changing (libpod's own cpuUtilization is computed once and never resampled) by computing it from /proc/stat deltas instead. - Fixed memory usage reading far too high by using /proc/meminfo's MemAvailable instead of libpod's raw (non-reclaimable-aware) memFree. - Added an Autostart Queue table reusing podman-autostart.sh's own failure-counter files. - Dashboard and Containers now auto-refresh every ~2s (paused when the tab is hidden or a modal is open). Toasts: - Real success/warn/error/info toast notifications replacing every alert() used for one-way feedback, across every panel. Container detail modal: - 5 new tabs: Resources, Logs, Console, Events, Healthcheck. Containers panel: - Folders to group containers (name + icon), stored in the plugin's own folders.json — a folder's header always shows an icon+name+status chip per member, matching Unraid's own Docker page folders. "Move to Folder" becomes "Remove from Folder" once a container is already grouped. - Containers can carry an icon URL and a WebUI URL (small button next to the name), both stored as container labels and auto-filled from templates where applicable. - Settings: an "Add container" control for the Autostart order table. Fixes: - Context menus now measure their own rendered size and flip above the anchor when there isn't room below, instead of running off-screen. - Containers table now uses table-layout:fixed with explicit column widths — auto layout was shifting every column (and the header) on every folder expand/collapse, and briefly again when a flex wrapper was mistakenly placed directly on a <td>. 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) {
|
|
P.toast('Remove failed: ' + err.message, 'error');
|
|
btn.disabled = false;
|
|
});
|
|
});
|
|
|
|
return load();
|
|
}
|
|
|
|
P.registerPanel('networks', { init: init, refresh: load });
|
|
})();
|