/** * javascript/app.js * * Shared runtime for the Podman plugin page: the AJAX helper every panel * module uses to talk to webui/plugins/podman/ajax/*.php, small DOM * utilities to avoid repeating the same escaping/formatting logic in ten * places, and the sub-tab router that shows/hides panels and lazily * initializes each one's module the first time it's opened. * * Loaded first (before any panel module) — see Podman.page. */ window.Podman = (function () { 'use strict'; const BASE = '/plugins/podman/ajax/'; /** * Calls one ajax/.php?action= endpoint and resolves with * response.data, or rejects with an Error carrying the server's message * — every ajax/*.php endpoint replies with the same {ok, data|error} * envelope (see include/helpers.php's podman_json_response/_error), so * this one function is the only place that envelope shape is known. * * @param {string} file e.g. "containers" * @param {string} action e.g. "list" * @param {('GET'|'POST')} method * @param {object|null} body sent as JSON for POST * @param {object} query extra query-string params (e.g. {id: "..."}) */ function call(file, action, method, body, query) { const params = new URLSearchParams(Object.assign({ action: action }, query || {})); const url = BASE + file + '.php?' + params.toString(); const opts = { method: method, headers: {} }; if (body !== undefined && body !== null) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body); } return fetch(url, opts) .then(function (res) { return res.json().then(function (envelope) { if (!envelope.ok) { throw new Error(envelope.error || ('Request failed (' + res.status + ')')); } return envelope.data; }); }); } function get(file, action, query) { return call(file, action, 'GET', null, query); } function post(file, action, body) { return call(file, action, 'POST', body || {}, {}); } // --- DOM helpers --------------------------------------------------------- function escapeHtml(value) { return String(value == null ? '' : value).replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } function el(id) { return document.getElementById(id); } function formatBytes(bytes) { if (!bytes || bytes <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const value = bytes / Math.pow(1024, i); return (value >= 100 || i === 0 ? value.toFixed(0) : value.toFixed(1)) + ' ' + units[i]; } function formatDuration(seconds) { if (seconds == null) return '—'; if (seconds < 60) return seconds + 's'; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (days > 0) return days + 'd ' + hours + 'h'; if (hours > 0) return hours + 'h ' + minutes + 'm'; return minutes + 'm'; } function formatRelativeTime(unixSeconds) { if (!unixSeconds) return '—'; const diff = Math.max(0, Math.floor(Date.now() / 1000) - unixSeconds); if (diff < 60) return 'just now'; if (diff < 3600) return Math.floor(diff / 60) + 'm ago'; if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'; return Math.floor(diff / 86400) + 'd ago'; } /** Status string (from libpod's container "State") -> chip color class. */ function stateChipClass(state) { switch (state) { case 'running': return 'podman-chip-good'; case 'paused': return 'podman-chip-warn'; case 'exited': case 'created': return 'podman-chip-neutral'; default: return 'podman-chip-bad'; } } function loadingRow(colspan, label) { return '' + escapeHtml(label || 'Loading…') + ''; } function errorRow(colspan, message) { return '' + escapeHtml(message) + ''; } // --- Panel router ---------------------------------------------------------- const panelModules = {}; const initialized = {}; /** Called by each panel's own JS file (e.g. containers.js) to register itself. */ function registerPanel(name, module) { panelModules[name] = module; } function activatePanel(name) { document.querySelectorAll('.podman-subnav button').forEach(function (btn) { btn.classList.toggle('active', btn.dataset.panel === name); }); document.querySelectorAll('.podman-panel').forEach(function (panel) { panel.classList.toggle('active', panel.id === 'podman-panel-' + name); }); const module = panelModules[name]; if (!module) return; if (!initialized[name]) { initialized[name] = true; if (typeof module.init === 'function') module.init(); } else if (typeof module.refresh === 'function') { module.refresh(); } } function boot() { const subnav = document.querySelector('.podman-subnav'); if (!subnav) return; subnav.addEventListener('click', function (e) { const btn = e.target.closest('button[data-panel]'); if (btn) activatePanel(btn.dataset.panel); }); const refreshBtn = el('podman-refresh-all'); if (refreshBtn) { refreshBtn.addEventListener('click', function () { const active = document.querySelector('.podman-subnav button.active'); if (active) activatePanel(active.dataset.panel); }); } // Activate whichever panel is marked active in the initial HTML // (Dashboard, by default — see Podman.page). const initial = document.querySelector('.podman-subnav button.active'); activatePanel(initial ? initial.dataset.panel : 'dashboard'); } document.addEventListener('DOMContentLoaded', boot); return { get: get, post: post, escapeHtml: escapeHtml, el: el, formatBytes: formatBytes, formatDuration: formatDuration, formatRelativeTime: formatRelativeTime, stateChipClass: stateChipClass, loadingRow: loadingRow, errorRow: errorRow, registerPanel: registerPanel, activatePanel: activatePanel, }; })();