💋 Sync 2026-10-04 04:16:09

This commit is contained in:
github-actions[bot] committed 2026-10-04 04:16:09 +08:00
1 parent f6b2df73ec
commit f62992fd80
745 files changed
+212416 -1173

No files matched your search

@@ -0,0 +1,388 @@
'use strict';
'require ui';
'require podman.ui as podmanUI';
'require podman.form as podmanForm';
'require podman.view as podmanView';
'require podman.rpc as podmanRPC';
'require podman.utils as utils';
'require podman.model.Container as Container';
const CENSORED_VALUE = '••••••••';
return podmanView.tabContent.extend({
tab: 'info',
container: null,
initScriptStatus: '',
async render(container) {
this.container = container;
const sections = await Promise.all([
this.basicSection(),
this.configSection(),
this.networkSection(),
this.envSection(),
this.mountSection(),
]);
this.updateInitScriptIcon(sections[0]);
return sections;
},
basicSection() {
const basicTable = new podmanUI.TableList();
const editableNameField = new ui.Textfield(this.container.getName(), { name: 'container-name' }).render();
const editableName = new podmanForm.EditableField('container-name', editableNameField);
editableName.onSubmit = (value) => this.handleUpdateName(value);
const policies = {
'no': _('No'),
'always': _('Always'),
'on-failure': _('On Failure'),
'unless-stopped': _('Unless Stopped')
};
const currentPolicy = this.container.getRestartPolicyName();
const editableRestartField = new ui.Select(currentPolicy, policies, { name: 'container-restart' }).render();
const editableRestart = new podmanForm.EditableField('container-restart', editableRestartField);
editableRestart.onSubmit = (value) => this.handleUpdateRestartPolicy(value);
basicTable
.addRow(_('Name'), editableName.render())
.addRow(_('ID'), this.container.getID().substring(0, 64))
.addRow(_('Image'), new podmanUI.Tooltip(this.container.getImageName(), this.container.Image, { class: 'tooltip' }).render())
.addRow(_('Tag'), this.container.getImageTag())
.addRow(_('Status'), this.container.getStateBadge())
.addRow(_('Created'), this.container.getCreated(true))
.addRow(_('Started'), this.container.getStartedAt(true))
.addRow(_('Restart Policy'), editableRestart.render())
.addRow(_('Auto Update'), this.container.getAutoUpdateLabel() || _('Disabled'))
.addRow(_('Init Script'), E('div', {
class: `init-script-form align-center ${this.container.getRestartPolicyName() === 'no' ? 'd-none' : 'd-flex'}`
}, [
new podmanUI.Tooltip(
new podmanUI.ButtonNew(
this.container.getAutoStartStatusIcon(),
{
click: ui.createHandlerFn(this, 'handleToggleInitScript'),
}
).render(),
_('Click to enable/disable'),
{
class: 'cursor-pointer',
}
).render(),
new podmanUI.ButtonNew(_('Show'), {
type: 'none',
click: ui.createHandlerFn(this, 'handleShowInitScript'),
}).render(),
new podmanUI.ButtonNew(_('Generate'), {
type: 'none',
click: ui.createHandlerFn(this, 'handleGenerateInitScript'),
}).render(),
]))
;
return E('div', {}, [
E('h4', {}, _('Basic Information')),
basicTable.render(),
]);
},
configSection() {
const configTable = new podmanUI.TableList();
const cmd = this.container.getCmdString() || '-';
const entrypoint = this.container.getEntrypointString() || '-';
const createCommand = this.container.getCreateCommandString() || '-';
const createCommandField = new podmanUI.SecretText(createCommand, CENSORED_VALUE);
configTable
.addRow(_('Create Command'), createCommandField.render())
.addRow(_('Command'), cmd)
.addRow(_('Entrypoint'), entrypoint)
.addRow(_('Working Directory'), this.container.getWorkingDir())
.addRow(_('User'), this.container.getUser())
.addRow(_('Hostname'), this.container.getHostname())
.addRow(_('Privileged'), this.container.getPrivileged() ? _('Yes') : _('No'))
.addRow(_('TTY'), this.container.getTty() ? _('Yes') : _('No'))
.addRow(_('Interactive'), this.container.getInteractive() ? _('Yes') : _('No'))
;
return E('div', {}, [
E('h4', {}, _('Configuration')),
configTable.render(),
]);
},
async networkSection () {
const table = new podmanUI.TableList();
const { NetworkMode, Links } = this.container.getHostConfig();
const { Networks, IPAddress } = this.container.getNetworkSettings();
table.addRow(_('Network Mode'), NetworkMode || _('default'));
const systemNetworks = ['bridge', 'host', 'none', 'container', 'slirp4netns'];
let userNetworks = [];
if (Networks && Object.keys(Networks).length > 0) {
userNetworks = Object.keys(Networks).filter((netName) => !systemNetworks.includes(netName));
userNetworks.forEach((netName) => {
const net = Networks[netName];
const tooltip = (() => {
if (!this.container.isRunning()) {
return _('Container is not running');
}
const parts = [];
if (net.IPAddress) parts.push(`IPv4: ${net.IPAddress}`);
if (net.GlobalIPv6Address) parts.push(`IPv6: ${net.GlobalIPv6Address}`);
else parts.push('IPv6: disabled');
if (net.Gateway) parts.push(`Gateway: ${net.Gateway}`);
if (net.MacAddress) parts.push(`MAC: ${net.MacAddress}`);
return parts.join('\n');
})();
const ipContent = net.GlobalIPv6Address
? [net.IPAddress || '-', E('br'), net.GlobalIPv6Address]
: net.IPAddress || '-';
table.addRow(
new podmanUI.Tooltip(E('span', { class: 'tooltip' }, netName), tooltip).render(),
E('div', { class: 'd-flex align-center' }, [
E('span', {}, ipContent),
new podmanUI.Tooltip('✖', _('Click to disconnect'), {
class: 'text-error cursor-pointer container-icon-disconnect ml-sm border-0',
click: () => this.handleNetworkDisconnect(netName),
}).render(),
])
);
});
}
if (IPAddress && userNetworks.length === 0) {
table.addRow(_('IP Address'), IPAddress);
}
const podmanNetworks = await podmanRPC.networks.list();
const networkOptions = Object.fromEntries(podmanNetworks.map(item => [item.name, item.name]));
const networkSelectWidget = new ui.Select([''], networkOptions, { name: 'container-connect-network', optional: false });
const ipTextWidget = new ui.Textfield('', { name: 'container-connect-ip', placeholder: '192.168.1.100', datatype: 'ip4addr' });
table.addRow(_('Connect to'), [
E('div', { class: 'd-flex align-center container-connect' }, [
networkSelectWidget.render(),
ipTextWidget.render(),
new podmanUI.Button(_('Connect'), () => {
const selectedNetwork = networkSelectWidget.getValue();
const selectedIp = ipTextWidget.getValue();
networkSelectWidget.triggerValidation();
ipTextWidget.triggerValidation();
if (!selectedNetwork || !ipTextWidget.isValid()) {
return;
}
this.handleNetworkConnect(selectedNetwork, selectedIp);
}).render(),
]),
]);
table.addRow(_('Ports'), E('div', {},
this.container.getPorts().map((port) => E('div', {}, port.string)),
));
if (Links && Links.length > 0) {
const linkNodes = Links.reduce((nodes, link, i) => {
if (i > 0) nodes.push(E('br'));
nodes.push(link);
return nodes;
}, []);
table.addRow(_('Links'), linkNodes);
}
return E('div', { class: 'networks' }, [
E('h4', {}, _('Network')),
table.render(),
]);
},
envSection() {
const table = new podmanUI.Table();
table
.addHeader(_('Variable'), { style: 'width: 50%;' })
.addHeader(_('Value'), { style: 'width: 50%;' });
this.container.getEnvironmentVars().forEach((env) => {
const parts = env.split('=');
const varName = parts[0];
const varValue = parts.slice(1).join('=');
const valueField = new podmanUI.SecretText(varValue, CENSORED_VALUE);
table.addRow([{
inner: varName
},
{
inner: valueField.render(),
},
]);
});
return E('div', {}, [
E('h4', {}, _('Environment Variables')),
table.render(),
]);
},
mountSection() {
const table = new podmanUI.Table();
table
.addHeader(_('Type'), { style: 'width: 10%;' })
.addHeader(_('Source'), { style: 'width: 35%;' })
.addHeader(_('Destination'), { style: 'width: 35%;' })
.addHeader(_('Mode'), { style: 'width: 10%;' })
;
const truncatedField = (value) => {
const v = value || '-';
return v.length > 50 ? new podmanUI.Tooltip(utils.truncate(v, 50), v).render() : v;
};
this.container.getMounts().forEach((mount) => {
const mountSourceField = truncatedField(mount.Source);
const mountDestField = truncatedField(mount.Destination);
table.addRow([
{ inner: mount.Type || '-' },
{ inner: mountSourceField },
{ inner: mountDestField },
{ inner: mount.RW ? 'rw' : 'ro' },
]);
})
return E('div', {}, [
E('h4', {}, _('Mounts')),
table.render(),
]);
},
async _refreshContainer() {
const inspectData = await this.container.inspect();
this.container = Container.getSingleton(inspectData);
},
async _refreshNetworkSection() {
const networkSection = await this.networkSection();
document.querySelector('.networks').replaceWith(networkSection);
},
async updateInitScriptIcon(infoSection) {
this.initScriptStatus = await this.container.checkInitScript();
const initScriptIconElement = (infoSection || document).querySelector('.autostart-status');
const initScriptIcon = this.container.getAutoStartStatusIcon(this.initScriptStatus);
initScriptIconElement.replaceWith(initScriptIcon);
},
async handleUpdateName(name) {
if (!name || this.container.Name === name) {
return;
}
this.loading(_('Update name'));
await this.container.updateName(name);
document.querySelector('.container-toolbar h2').textContent = name;
this.success(_('Name updated successfully'));
},
async handleUpdateRestartPolicy(policy) {
this.loading(_('Update restart policy'));
const updateData = { RestartPolicy: policy };
if (policy === 'on-failure') updateData.RestartRetries = 5;
await this.container.update(updateData);
await this._refreshContainer();
const initScriptForm = document.querySelector('.init-script-form');
if (policy === 'no') {
initScriptForm.classList.replace('d-flex', 'd-none');
} else {
initScriptForm.classList.replace('d-none', 'd-flex');
}
this.updateInitScriptIcon();
this.success(_('Restart policy updated successfully'));
},
async handleNetworkConnect(networkName, ip) {
this.loading(_('Connecting container to network'));
const params = {};
if (ip) params.static_ips = [ip];
await this.container.connect(networkName, params);
await this._refreshContainer();
this.success(_('Connected successfully to %s').format(networkName));
await this._refreshNetworkSection();
},
async handleNetworkDisconnect(networkName) {
this.loading(_('Disconnect container from network'));
await this.container.disconnect(networkName);
await this._refreshContainer();
this.success(_('Disconnected from %s').format(networkName));
await this._refreshNetworkSection();
},
async handleGenerateInitScript() {
this.loading(_('Creating auto-start configuration for %s').format(this.container.getName()));
await this.container.generateInitScript();
await this.container.enableInitScript();
ui.hideModal();
window.location.reload();
},
async handleShowInitScript() {
this.loading(_('Loading init script'));
const result = await this.container.showInitScript();
ui.hideModal();
if (!result || !result.path || !result.content) return;
const modal = new podmanUI.Modal(result.path, [
new podmanUI.BashCodeArea(result.content.replace(/\\n/g, '\n')).render()
]);
modal.handleSubmit = undefined;
modal.render();
},
async handleToggleInitScript() {
if (this.initScriptStatus === 'missing') {
return this.handleGenerateInitScript();
} else if (this.initScriptStatus === 'enabled') {
this.loading(_('Disabling init script'));
await this.container.disableInitScript();
ui.hideModal();
window.location.reload();
} else if (this.initScriptStatus === 'disabled') {
this.loading(_('Enabling init script'));
await this.container.enableInitScript();
ui.hideModal();
window.location.reload();
}
},
});
@@ -0,0 +1,137 @@
'use strict';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
const TIMESTAMP_RE_GLOBAL = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\s*/g;
const ANSI_REGEX = /\x1B\[[0-9;]*[A-Za-z]/g;
const MAX_LOG_LINES = 1000;
return podmanView.tabContent.extend({
tab: 'logs',
container: null,
render(container) {
this.container = container;
this.logViewer = E('pre', { id: 'log-viewer', class: 'terminal-area' }, []);
return this.renderTabContent('', [
this.renderLogToolbar(),
this.logViewer,
]);
},
onTabActive() {
// Running: auto-start live stream. Stopped: load historical logs on demand via play button.
if (this.container.isRunning()) {
this.startStream();
}
},
onTabInactive() {
this.stopStream();
},
_setStreamActive(active) {
this.logLinesInput.disabled = active;
this.logSinceInput.disabled = active;
this.logUntilInput.disabled = active;
this.playButton.querySelector('.cbi-button').classList.toggle('cbi-button-active', active);
this.stopButton.querySelector('.cbi-button').classList.toggle('cbi-button-active', !active);
},
startStream() {
if (!this.container || this.logsStream) {
return;
}
this._setStreamActive(true);
this.logViewer.textContent = '';
const toUnixSince = (val) => val ? new Date(val + 'T00:00:00').getTime() / 1000 : null;
const toUnixUntil = (val) => val ? new Date(val + 'T23:59:59').getTime() / 1000 : null;
this.logsStream = this.container.streamLogs((data) => {
if (!data || !data.raw) return;
const line = data.raw
.replace(TIMESTAMP_RE_GLOBAL, '')
.replace(ANSI_REGEX, '')
.trim();
if (!line) return;
this.logViewer.textContent += line + '\n';
this.trimLogOutput();
this.logViewer.scrollTop = this.logViewer.scrollHeight;
}, {
tail: this.logLinesInput.value || 100,
since: toUnixSince(this.logSinceInput.value),
until: toUnixUntil(this.logUntilInput.value),
follow: this.container.isRunning(),
});
},
stopStream() {
if (!this.logsStream) return;
this._setStreamActive(false);
this.logsStream.stop();
this.logsStream = null;
},
renderLogToolbar() {
// Fields start disabled only for running containers (stream auto-starts and locks them).
// For stopped containers they start enabled so the user can set filters before fetching.
const initialDisabled = this.container.isRunning();
const linesField = new podmanUI.Numberfield(100, { id: 'log-lines', min: '10', max: '250', disabled: initialDisabled }).render();
const sinceField = new podmanUI.Datefield(null, { id: 'log-since', disabled: initialDisabled }).render();
const untilField = new podmanUI.Datefield(null, { id: 'log-until', disabled: initialDisabled }).render();
this.logLinesInput = linesField.querySelector('input');
this.logSinceInput = sinceField.querySelector('input');
this.logUntilInput = untilField.querySelector('input');
this.stopButton = new podmanUI.ButtonNew('◼', {
click: () => this.stopStream(),
type: ' stop-button',
tooltip: _('Stop stream'),
}).render();
this.playButton = new podmanUI.ButtonNew('►', {
click: () => this.startStream(),
type: 'active play-button',
tooltip: _('Start stream'),
}).render();
return E('div', { class: 'd-flex align-center mb-sm' }, [
E('label', { class: 'mr-xs' }, _('Number of lines')),
linesField,
'',
E('label', { class: 'ml-xs mr-xs' }, _('Since')),
sinceField,
E('label', { class: 'ml-xs mr-xs' }, _('Until')),
untilField,
E('div', { class: 'ml-xs' }, ''),
new podmanUI.ButtonNew('🗑️', {
click: () => this.logViewer.textContent = '',
tooltip: _('Clear logs'),
}).render(),
this.stopButton,
this.playButton,
]);
},
trimLogOutput() {
const lines = this.logViewer.textContent.split('\n');
if (lines.length > MAX_LOG_LINES) {
this.logViewer.textContent = lines.slice(lines.length - MAX_LOG_LINES).join('\n');
}
},
});
@@ -0,0 +1,99 @@
'use strict';
'require dom';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
return podmanView.tabContent.extend({
tab: 'ps',
container: null,
render(container) {
this.container = container;
if (!this.container.isRunning()) {
return this.warningContent(_('Container is not running'));
}
this.tableContent = E('div', { class: 'ps-table-content' }, []);
return this.renderTabContent('', [
this.tableContent,
]);
},
onTabActive() {
if (!this.container || !this.container.isRunning() || this.processStream) {
return;
}
this.processStream = this.container.streamTop((ps) => {
this.updateProcessList(ps);
});
},
onTabInactive() {
if (!this.processStream) return;
this.processStream.stop();
this.processStream = null;
},
updateProcessList(ps) {
if (!ps || !ps.Titles || !ps.Processes || ps.Titles.length === 0) {
return;
}
const psTable = new podmanUI.Table();
const columnWidth = `width: ${100 / ps.Titles.length}%;`;
const timeIndex = ps.Titles.indexOf('ELAPSED');
ps.Titles.forEach((title) => {
psTable.addHeader(_(title), { style: columnWidth });
});
ps.Processes.forEach((process) => {
psTable.addRow(process.map((detail, i) => ({
inner: i === timeIndex ? this._formatElapsedTime(detail) : detail
})));
});
dom.content(this.tableContent, psTable.render());
},
_formatElapsedTime(timeStr) {
if (!timeStr) return '-';
const result = [];
const pattern = /(\d+(?:\.\d+)?)([ydhms])/g;
let match;
while ((match = pattern.exec(timeStr)) !== null) {
let value = parseFloat(match[1]);
const unit = match[2];
if (unit === 's' && value > 1000) {
value = Math.floor(value / 1000000000);
if (value >= 60) {
const mins = Math.floor(value / 60);
const secs = value % 60;
if (mins > 0) result.push(`${mins}m`);
if (secs > 0) result.push(`${secs}s`);
continue;
}
} else {
value = Math.floor(value);
}
if (unit === 's' || unit === 'm') {
result.push(String(value).padStart(2, '0') + unit);
} else {
result.push(value + unit);
}
}
return result.length > 0 ? result.join('') : timeStr;
}
});
@@ -0,0 +1,107 @@
'use strict';
'require dom';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
'require podman.utils as podmanUtil';
return podmanView.tabContent.extend({
tab: 'stats',
container: null,
render(container) {
this.container = container;
if (!this.container.isRunning()) {
return this.warningContent(_('Container is not running'));
}
return this.renderTabContent(_('Statistics'), [
this.renderStatsTable(),
]);
},
onTabActive() {
if (!this.container || !this.container.isRunning() || this.statsStream) {
return;
}
this.statsStream = this.container.streamStats((stats) => {
this.updateStatsDisplay(stats);
});
},
onTabInactive() {
if (!this.statsStream) {
return;
}
this.statsStream.stop();
this.statsStream = null;
},
renderStatsTable() {
const table = new podmanUI.TableList();
table
.addRow(_('CPU Usage'), '-', { 'data-stat': 'cpu' })
.addRow(_('Memory Usage'), '-', { 'data-stat': 'memory' })
.addRow(_('Memory Limit'), '-', { 'data-stat': 'memory-limit' })
.addRow(_('Memory %'), '-', { 'data-stat': 'memory-percent' })
.addRow(_('Network I/O'), '-', { 'data-stat': 'network-io' })
.addRow(_('Block I/O'), '-', { 'data-stat': 'block-io' })
.addRow(_('PIDs'), '-', { 'data-stat': 'pids' })
;
return table.render();
},
updateStatsDisplay(stats) {
if (!stats) return;
if (!this.statElements) {
this.statElements = {};
for (const key of ['cpu', 'memory', 'memory-limit', 'memory-percent', 'network-io', 'block-io', 'pids']) {
this.statElements[key] = document.querySelector(`[data-stat="${key}"] td:last-of-type`);
}
}
const updates = [
['cpu', stats.CPU != null ? stats.CPU.toFixed(2) + '%' : '-'],
['memory', podmanUtil.format.bytes(stats.MemUsage) || '-'],
['memory-limit', podmanUtil.format.bytes(stats.MemLimit) || '-'],
['memory-percent', stats.MemPerc != null ? stats.MemPerc.toFixed(2) + '%' : '-'],
['network-io', this._formatNetworkIO(stats.Network)],
['block-io', this._formatBlockIO(stats.BlockInput, stats.BlockOutput)],
['pids', stats.PIDs],
];
for (const [key, value] of updates) {
if (this.statElements[key])
dom.content(this.statElements[key], value);
}
},
_formatNetworkIO(networks) {
const parts = Object.keys(networks || {}).map((iface) => {
const net = networks[iface];
return `${iface}: ↓ ${podmanUtil.format.bytes(net.RxBytes)} / ↑ ${podmanUtil.format.bytes(net.TxBytes)}`;
});
if (parts.length === 0) return '-';
return parts.reduce((nodes, part, i) => {
if (i > 0) nodes.push(E('br'));
nodes.push(part);
return nodes;
}, []);
},
_formatBlockIO(blockInput, blockOutput) {
return _('Read: %s / Write: %s').format(
podmanUtil.format.bytes(blockInput || 0),
podmanUtil.format.bytes(blockOutput || 0)
);
}
});
@@ -0,0 +1,167 @@
'use strict';
'require ui';
'require dom';
'require podman.constants as constant';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
'require podman.model.Container as Container';
'require podman.form.resource as PodmanFormResource';
'require view.podman.container-tab.info as ContainerInfoTab';
'require view.podman.container-tab.stats as ContainerStatsTab';
'require view.podman.container-tab.processes as ContainerProcessesTab';
'require view.podman.container-tab.logs as ContainerLogsTab';
/**
* Container detail view with tabbed interface
*/
return podmanView.container.extend({
container: null,
data: null,
listUrl: L.url('admin/podman/containers'),
async load() {
// Extract container ID from URL path
// URL format: /cgi-bin/luci/admin/podman/container/<id>
const path = L.location();
const matches = path.match(/container\/([a-f0-9]+)/i);
if (!matches || !matches[1]) {
this.redirectToList();
return;
}
const containerId = matches[1];
const container = Container.getSingleton({ Id: containerId });
const inspectData = await container.inspect();
this.data = inspectData;
return Container.getSingleton(inspectData);
},
async render(container) {
if (!container) {
this.redirectToList();
return;
}
this.container = container;
this.tabs
.addTab('info', _('Info'))
.addTab('resources', _('Resources'))
.addTab('stats', _('Stats'))
.addTab('ps', _('Processes'))
.addTab('logs', _('Logs'))
// .addTab('health', _('Health'))
.addTab('inspect', _('Inspect'))
// .addTab('console', _('Console'))
;
requestAnimationFrame(() => {
this.renderInfoTab();
this.renderResourcesTab();
this.renderStatsTab();
this.renderProcessesTab();
this.renderLogsTab();
this.renderInspectTab();
});
return this.super('render', []);
},
createHeader() {
return this.super('createHeader', [
this.container.getName(),
this.container.isRunning(),
this.container.isStopped(),
this.container.isPaused(),
]);
},
stopStreams() {
this.getTabInstance('stats')?.onTabInactive();
this.getTabInstance('ps')?.onTabInactive();
this.getTabInstance('logs')?.onTabInactive();
},
async renderInfoTab() {
const content = await ContainerInfoTab.render(this.container);
this.renderTab('info', content);
},
async renderResourcesTab() {
const resourceForm = await new PodmanFormResource.init().render(this.container);
this.renderTab('resources', resourceForm, _('Configure resource limits for this container.'));
},
async renderStatsTab() {
const content = await ContainerStatsTab.render(this.container);
this.renderTab('stats', content);
},
async renderProcessesTab() {
const content = await ContainerProcessesTab.render(this.container);
this.renderTab('ps', content);
},
async renderLogsTab() {
const content = await ContainerLogsTab.render(this.container);
this.renderTab('logs', content);
},
renderInspectTab() {
this.renderTab('inspect', new podmanUI.JsonArea(this.data).render());
},
async handleStart() {
if (this.container.isRunning()) {
return;
}
this.loading(_('Start container'));
if (this.container.isPaused()) {
this.container.unpause().then(() => window.location.reload());
return;
}
this.container.start().then(() => window.location.reload());
},
async handleStop() {
if (this.container.isStopped()) {
return;
}
this.loading(_('Stop container'));
this.stopStreams();
this.container.stop().then(() => window.location.reload());
},
async handleRestart() {
this.loading(_('Restart container'));
this.stopStreams();
this.container.restart().then(() => window.location.reload());
},
async handlePause() {
if (!this.container.isRunning()) {
return;
}
this.loading(_('Pause container'));
this.stopStreams();
this.container.pause().then(() => window.location.reload());
},
async handleRemove() {
this.confirm([
E('p', {}, _('Are you sure to remove container?')),
], async () => {
this.loading(_('Remove container'));
this.container.remove().then(() => this.redirectToList());
});
},
});
@@ -0,0 +1,151 @@
'use strict';
'require dom';
'require ui';
'require podman.constants as constant';
'require podman.rpc as podmanRPC';
'require podman.utils as utils';
'require podman.ui as podmanUI';
'require podman.form as podmanForm';
'require podman.view as podmanView';
'require podman.form.container as PodmanFormContainer';
/**
* Manage podman containers
*/
return podmanView.list.extend({
sectionName: 'containers',
title: _('Containers'),
titleSingle: _('Container'),
async load() {
return podmanRPC.containers.list('all=true');
},
/**
* Update list section
*/
async updateSection() {
this.section.selectKey = 'Id';
this.section.createForm = PodmanFormContainer;
this.section.handleRefreshCb = () => this.checkInitScripts();
this.section.toolbarExtraButtons = [
new podmanUI.Button(constant.ICON.START, ui.createHandlerFn(this, 'handleStart')).render(),
new podmanUI.Button(constant.ICON.STOP, ui.createHandlerFn(this, 'handleStop')).render(),
new podmanUI.Button(constant.ICON.RESTART, ui.createHandlerFn(this, 'handleRestart')).render(),
new podmanUI.Button(constant.ICON.PAUSE, ui.createHandlerFn(this, 'handlePause')).render(),
];
let o;
o = this.section.option(podmanForm.field.DummyValue, 'Names', _('Name'));
o = this.section.option(podmanForm.field.DummyValue, 'Id', _('ID'));
o.cfgdatavalue = (container) => container.getDetailLink(utils.truncate(container.getID(), 10));
o.width = '12%';
o = this.section.option(podmanForm.field.DummyValue, 'Image', _('Image'));
o.cfgdatavalue = (container) => container.getImageName();
o.width = '25%';
o = this.section.option(podmanForm.field.DummyValue, 'ImageTag', _('Tag'));
o.cfgdatavalue = (container) => container.getImageTag();
o.cfgformatter = (imageTag) => utils.truncate(imageTag, 10);
o.cfgtt = (imageTag) => imageTag.length > 10 ? imageTag : '';
o.width = '11%';
o = this.section.option(podmanForm.field.DummyValue, 'State', _('Status'));
o.cfgdatavalue = (container) => container.getStateBadge();
o.width = '8%';
o = this.section.option(podmanForm.field.TimestampDummyValue, 'StartedAt', _('Started At'));
o.width = '17%';
o = this.section.option(podmanForm.field.DummyValue, 'InitScript', _('Boot'));
o.cfgvalue = () => E('span', { class: 'autostart-status autostart-' }, '...');
o.width = '6%';
},
transformMap(map) {
this.checkInitScripts(map);
return map;
},
async checkInitScripts(renderedTable) {
const data = this.map.data.data;
for (const [key, container] of Object.entries(data)) {
const iconEl = (renderedTable || document)
.querySelector(`tr[data-sid="${key}"] td[data-name="InitScript"] .autostart-status`);
container.checkInitScript().then((initScriptStatus) => {
let initScriptIcon = container.getAutoStartStatusIcon(initScriptStatus);
if (initScriptStatus === 'missing') {
initScriptIcon = new podmanUI.Tooltip(
initScriptIcon,
_('Start script missing\n\nClick to generate'),
{
class: 'border-0',
click: ui.createHandlerFn(this, 'handleGenerateInitScript')
}
).render();
} else if (initScriptStatus === 'disabled') {
initScriptIcon = new podmanUI.Tooltip(initScriptIcon, _('Start script disabled'), { class: 'border-0' }).render();
} else if (initScriptStatus === 'enabled') {
initScriptIcon = new podmanUI.Tooltip(initScriptIcon, _('Start on boot'), { class: 'border-0' }).render();
}
if (iconEl) iconEl.replaceWith(initScriptIcon);
}).catch(() => {});
}
},
async handleGenerateInitScript(event) {
const clickedIcon = event.target;
const recordRow = dom.parent(clickedIcon, 'tr');
const container = this.map.data.data[recordRow.getAttribute('data-sid')];
await container.generateInitScript();
await container.enableInitScript();
this.section.handleRefresh();
},
handleStart() {
return this.updateContainersStatus(
(container) => container.isPaused() ? container.unpause() : container.start(),
_('Start container')
);
},
handleStop() {
return this.updateContainersStatus((container) => container.stop(), _('Stop container'));
},
handleRestart() {
return this.updateContainersStatus((container) => container.restart(), _('Restart container'));
},
handlePause() {
return this.updateContainersStatus((container) => container.pause(), _('Pause container'));
},
async updateContainersStatus(statusFunction, textLoad) {
const section = this.getSection();
const selected = section.getSelectedData();
if (selected.length === 0) {
return section.showNoneSelectedWarning();
}
for (const [i, container] of selected.entries()) {
this.loading(_('%s: %d/%d').format(textLoad, i + 1, selected.length));
await statusFunction(container);
}
ui.hideModal();
section.handleRefresh();
},
});
@@ -0,0 +1,118 @@
'use strict';
'require form';
'require ui';
'require podman.rpc as podmanRPC';
'require podman.utils as utils';
'require podman.ui as podmanUI';
'require podman.form as podmanForm';
'require podman.view as podmanView';
'require podman.form.image as PodmanFormImage';
const IMAGE_ID_TRUNCATE_LEN = 12;
const IMAGE_TAG_TRUNCATE_LEN = 14;
/**
* Manage podman images
*/
return podmanView.list.extend({
sectionName: 'images',
title: _('Images'),
titleSingle: _('Image'),
async load() {
return podmanRPC.images.list();
},
/**
* Update list section
*/
async updateSection() {
this.section.selectKey = 'Id';
this.section.handleCreateText = _('Pull Image');
this.section.getCreateButton = function() {
return new podmanUI.ButtonNew(_('Pull Image'), {
click: ui.createHandlerFn(this, 'handleCreate'),
type: 'add',
}).render();
};
this.section.modalCreateTitle = _('Pull Image');
this.section.createForm = PodmanFormImage;
this.section.toolbarExtraButtons = [
new podmanUI.Button(_('Update'), ui.createHandlerFn(this, 'handlePullImages')).render(),
];
let o;
o = this.section.option(podmanForm.field.DummyValue, 'Repository', _('Repository'));
o.cfgdatavalue = async (image) => image.getRepository();
o = this.section.option(podmanForm.field.DummyValue, 'Tag', _('Tag'));
o.cfgdatavalue = async (image) => {
const tagValue = image.getTag();
if (tagValue.length > IMAGE_TAG_TRUNCATE_LEN) {
return new podmanUI.Tooltip(utils.truncate(tagValue, IMAGE_TAG_TRUNCATE_LEN), tagValue).render();
}
return tagValue;
};
o.width = '17%';
o = this.section.option(podmanForm.field.LinkDummyValue, 'ImageId', _('Image ID'));
o.click = (_value, image) => this.section.handleInspect(image);
o.cfgdatavalue = (image) => utils.truncate(image.getID(), IMAGE_ID_TRUNCATE_LEN);
o.width = '15%';
o = this.section.option(podmanForm.field.ByteDummyValue, 'Size', _('Size'));
o.width = '12%';
o = this.section.option(podmanForm.field.TimestampDummyValue, 'Created', _('Created'));
o.width = '18%';
return this.map.render();
},
handlePullImages: async function () {
const selected = this.section.getSelectedData();
if (selected.length === 0) {
return this.section.showNoneSelectedWarning();
}
const progressEl = E('pre', { class: 'terminal-area' });
const modal = new podmanUI.Modal(_('Updating Images'), [ progressEl ]);
/** @type {HTMLElement} */
const closeButton = modal.getCloseButton();
closeButton.classList.add('d-none');
modal.getButtons = () => [ closeButton ];
modal.render();
const append = (text) => {
progressEl.textContent += text;
progressEl.scrollTop = progressEl.scrollHeight;
};
let errors = 0;
let first = true;
for (const image of selected) {
if (!first) append('\n');
first = false;
try {
await image.streamPull((text) => append(text));
} catch (err) {
podmanUI.alert(`${_('Error')}: ${err.message}`);
errors++;
}
}
this.section.handleRefresh();
closeButton.classList.remove('d-none');
if (errors === 0)
podmanUI.alert(_('All images updated successfully'), 'success', true);
},
});
@@ -0,0 +1,216 @@
'use strict';
'require ui';
'require podman.rpc as podmanRPC';
'require podman.ui as podmanUI';
'require podman.model.Container as Container';
return podmanUI.Modal.extend({
title: _('Auto Update'),
containers: [],
buttons: [],
getContent() {
if (!this.containers || this.containers.length === 0) {
this.buttons = [ this.getCloseButton() ];
return [
E('p', {}, _('No containers with auto-update label found.')),
E('p', { class: 'mt-sm' },
_('To enable auto-update for a container, add the label: io.containers.autoupdate=registry')
),
];
}
this.buttons = [
this.getCloseButton(),
new podmanUI.ButtonNew(_('Check'), {
click: ui.createHandlerFn(this, 'handleCheckUpdate'),
type: 'positive',
}).render()
];
return [
E('p', {}, _('Check %d containers for updates?').format(this.containers.length))
];
},
setContainers(containers) {
this.containers = containers ?? [];
return this;
},
getButtons() {
return this.buttons;
},
getCloseButton() {
return new podmanUI.ButtonNew(_('Close'), {
click: () => ui.hideModal(),
type: 'remove',
}).render();
},
async handleCheckUpdate() {
const containersWithUpdates = [];
const checkErrors = [];
for (const [index, container] of this.containers.entries()) {
podmanUI.showSpinningModal(null, _('Check for updates: %s/%s').format(index + 1, this.containers.length));
const updateState = await container.checkImageUpdate();
if (updateState.hasUpdate) {
containersWithUpdates.push(container);
} else if (updateState.error) {
checkErrors.push({ name: container.getName(), error: updateState.error });
}
}
if (containersWithUpdates.length === 0) {
const content = [ E('p', {}, _('No new updates available.')) ];
if (checkErrors.length > 0) {
content.push(E('p', { class: 'mt-sm' }, _('%d check(s) failed:').format(checkErrors.length)));
checkErrors.forEach((e) => content.push(E('p', { class: 'text-error' }, `${e.name}: ${e.error}`)));
}
const modal = new podmanUI.Modal(_('Check Complete'), content);
modal.getButtons = () => [ this.getCloseButton() ];
modal.render();
return;
}
const updateContent = [
E('p', {}, _('Select containers to update')),
];
const checkboxes = {};
containersWithUpdates.forEach((container) => {
const skipReason = container.getUpdateSkipReason();
const checkbox = new ui.Checkbox(0);
const checkboxNode = checkbox.render();
const checkboxInputNode = checkboxNode.querySelector('input[type="checkbox"]');
const checkboxId = checkboxInputNode.id;
if (skipReason) {
checkboxInputNode.disabled = true;
} else {
checkboxes[container.getID()] = { container, checkbox };
}
const inner = skipReason
? new podmanUI.Tooltip(checkboxNode, skipReason).render()
: checkboxNode;
updateContent.push(E('div', { class: 'd-flex align-center checkbox-with-label mb-xs' }, [
inner,
E('label', { for: checkboxId }, container.getName()),
]));
});
const modal = new podmanUI.Modal(_('Apply Updates'), updateContent);
modal.getButtons = () => [
this.getCloseButton(),
new podmanUI.ButtonNew(_('Update'), {
click: async () => {
const selectedUpdates = Object.values(checkboxes)
.filter(({ checkbox }) => checkbox.isChecked())
.map(({ container }) => container);
if (selectedUpdates.length === 0) {
return;
}
await this.handleUpdate(selectedUpdates);
},
type: 'positive',
}).render()
];
modal.render();
},
async handleUpdate(containers) {
const failures = [];
const oldImages = [];
const counterEl = E('strong', {}, '');
const spinnerEl = E('div', { class: 'spinning' });
const headerEl = E('div', { class: 'd-flex align-center mb-sm gap-5' }, [
counterEl,
spinnerEl,
]);
const log = new podmanUI.StreamLog();
const finalize = () => {
ui.hideModal();
if (failures.length === 0 && imageWarnings.length === 0) {
podmanUI.alert(_('Containers updated successfully'), 'success', true);
return;
}
const lines = [];
if (failures.length > 0) {
const succeeded = containers.length - failures.length;
lines.push(E('p', {}, _('%d updated, %d failed:').format(succeeded, failures.length)));
failures.forEach(f => lines.push(E('p', {}, `${f.name}: ${f.error}`)));
} else {
lines.push(E('p', {}, _('Containers updated successfully.')));
}
imageWarnings.forEach(w => lines.push(E('p', { class: 'mt-sm' }, w)));
podmanUI.alert(lines, failures.length > 0 ? 'warning' : 'info');
};
const closeBtn = new podmanUI.ButtonNew(_('Close'), {
click: () => finalize(),
type: 'negative',
}).render();
closeBtn.disabled = true;
const modal = new podmanUI.Modal(_('Apply Updates'), [ headerEl, log.render() ]);
modal.getButtons = () => [ closeBtn ];
modal.render();
let imageWarnings = [];
for (const [i, container] of containers.entries()) {
counterEl.textContent = _('Update image: %d/%d - %s').format(i + 1, containers.length, container.getName());
log.append('━━ ' + container.getName() + ' ━━\n');
try {
const inspected = Container.getSingleton(await container.inspect());
const result = await inspected.updateImage((line) => log.append(line));
if (result?.oldImage) oldImages.push({ image: result.oldImage });
} catch (err) {
const msg = err.message || String(err);
log.append('✗ ' + msg + '\n');
failures.push({ name: container.getName(), error: msg });
}
log.append('\n');
}
if (oldImages.length > 0) {
counterEl.textContent = _('Cleaning up old images...');
log.append('━━ ' + _('Cleaning up old images...') + ' ━━\n');
const allContainers = await podmanRPC.containers.list('all=true').catch(() => []);
const normalize = id => (id || '').replace(/^sha256:/, '').toLowerCase();
const usedIds = new Set(allContainers.map(c => normalize(c.ImageID)));
const seen = new Set();
for (const { image } of oldImages) {
const id = normalize(image.getID());
if (seen.has(id)) continue;
seen.add(id);
if (usedIds.has(id)) {
const warning = _('Old image still used by other containers');
imageWarnings.push(warning);
log.append('! ' + warning + '\n');
} else {
await image.remove();
log.append('✓ ' + _('Removed old image %s').format(image.getID().substring(0, 12)) + '\n');
}
}
}
counterEl.textContent = _('Done.');
spinnerEl.classList.remove('spinning');
closeBtn.disabled = false;
},
});
@@ -0,0 +1,120 @@
'use strict';
'require ui';
'require podman.rpc as podmanRPC';
'require podman.utils as podmanUtil';
'require podman.ui as podmanUI';
return podmanUI.Modal.extend({
title: _('Cleanup Unused Resources'),
getContent() {
this.checkboxPruneAll = new ui.Checkbox(1, {
hiddenname: 'prune-all-images'
});
this.checkboxPruneVolumes = new ui.Checkbox(0, {
hiddenname: 'prune-volumes'
});
const pruneAllNode = this.checkboxPruneAll.render();
const pruneVolumesNode = this.checkboxPruneVolumes.render();
const pruneAllId = pruneAllNode.querySelector('input[type="checkbox"]').id;
const pruneVolumesId = pruneVolumesNode.querySelector('input[type="checkbox"]').id;
return E('div', {}, [
E('p', {}, _('Select what to clean up:')),
E('div', {
class: 'd-flex align-center checkbox-with-label',
style: 'margin-bottom: 2px;'
}, [
pruneAllNode,
E('label', {
for: pruneAllId
}, _('Remove all unused images (not just dangling)')),
]),
E('div', {
class: 'd-flex align-center checkbox-with-label'
}, [
pruneVolumesNode,
E('label', {
for: pruneVolumesId
}, _('Remove unused volumes')),
]),
]);
},
getButtons() {
return [
this.getCloseButton(),
new podmanUI.ButtonNew(_('Clean Up Now'), {
click: ui.createHandlerFn(this, 'handlePrune'),
type: 'positive',
}).render()
];
},
async handlePrune() {
const allImages = this.checkboxPruneAll.getValue() === '1';
const volumes = this.checkboxPruneVolumes.getValue() === '1';
podmanUI.showSpinningModal(_('Clean Up Now'), _(
'Removing unused resources, please wait...'));
const result = await podmanRPC.system.prune(allImages, volumes);
let freedSpace = 0;
const deletedItems = [];
const reportTypes = [{
key: 'ContainerPruneReports',
label: _('Containers')
},
{
key: 'ImagePruneReports',
label: _('Images')
},
{
key: 'VolumePruneReports',
label: _('Volumes')
}
];
reportTypes.forEach((type) => {
const reports = result[type.key];
if (reports && reports.length > 0) {
reports.forEach((r) => {
if (r.Size > 0) freedSpace += r.Size;
});
deletedItems.push('%d %s'.format(reports.length, type.label
.toLowerCase()));
}
});
const modal = new podmanUI.Modal(_('Cleanup Complete'));
modal.handleClose = null;
modal.handleSubmit = () => {
ui.hideModal();
window.location.reload();
};
modal.content = [
E('p', {}, _('Cleanup successful!')),
];
if (deletedItems.length > 0) {
modal.content.push(E('p', {
class: 'mt-sm'
}, _('Removed: %s').format(deletedItems.join(', '))));
} else {
modal.content.push(E('p', {
class: 'mt-sm'
}, _('No unused resources found')));
}
modal.content.push(E('p', {
class: 'mt-sm text-success text-bold'
}, _('Space freed: %s').format(podmanUtil.format.bytes(freedSpace))));
modal.render();
},
});
@@ -0,0 +1,128 @@
'use strict';
'require network';
'require ui';
'require podman.rpc as podmanRPC';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
'require podman.form as podmanForm';
'require podman.form.network as PodmanFormNetwork';
/**
* Manage podman networks
*/
return podmanView.list.extend({
sectionName: 'networks',
title: _('Networks'),
titleSingle: _('Network'),
async load() {
return podmanRPC.networks.list();
},
/**
* Update list section
*/
async updateSection() {
this.section.selectKey = 'ID';
this.section.createForm = PodmanFormNetwork;
this.section.handleRemove = async () => this.removeNetworks();
let o;
o = this.section.option(podmanForm.field.LinkDummyValue, 'name', _('Name'));
o.click = (_value, net) => this.section.handleInspect(net);
o = this.section.option(podmanForm.field.DummyValue, 'Int', _('Int'));
o.cfgdatavalue = (net) => this.getIntegrationIcon(net);
o.width = '5%';
o = this.section.option(podmanForm.field.DummyValue, 'driver', _('Driver'));
o.width = '10%';
const ipPairCell = (getV4, getV6) => (net) => E('div', {}, [
E('span', {}, getV4(net) || '-'),
E('br'),
E('span', {}, getV6(net) || '-'),
]);
o = this.section.option(podmanForm.field.DummyValue, 'Subnet', _('Subnet'));
o.cfgdatavalue = ipPairCell(n => n.getSubnetIP4(), n => n.getSubnetIP6());
o.width = '20%';
o = this.section.option(podmanForm.field.DummyValue, 'Gateway', _('Gateway'));
o.cfgdatavalue = ipPairCell(n => n.getGatewayIP4(), n => n.getGatewayIP6());
o.width = '20%';
o = this.section.option(podmanForm.field.DateDummyValue, 'created', _('Created'));
o.width = '20%';
await network.flushCache();
},
async removeNetworks() {
const selected = this.section.getSelectedData();
if (selected.length === 0) {
return this.section.showNoneSelectedWarning();
}
const checkboxRemoveIntegration = new ui.Checkbox('1', { hiddenname: 'remove-integration' });
const checkboxNode = checkboxRemoveIntegration.render();
const checkboxId = checkboxNode.querySelector('input[type="checkbox"]').id;
this.confirm([
E('p', {}, _('Are you sure to delete the records?')),
E('div', { class: 'd-flex align-center checkbox-with-label' }, [
checkboxNode,
E('label', { for: checkboxId }, _('Remove OpenWrt integration')),
]),
], async () => {
for (const [i, item] of selected.entries()) {
this.loading(_('Deleting records: %s/%s').format(i + 1, selected.length));
await item.remove();
if (checkboxRemoveIntegration.getValue() === '1') {
await item.integrationRemove();
}
}
ui.hideModal();
this.section.handleRefresh();
});
},
async getIntegrationIcon(net) {
const { hasInterface, hasDevice, hasDnsmasqExclusion } = await net.integrationCheck();
if (hasInterface && hasDevice && hasDnsmasqExclusion) {
return new podmanUI.Tooltip('✅', _('OpenWrt integration exists\n\nClick to remove'), {
click: () => {
this.confirm([
E('p', {}, _('Are you sure to remove integration?')),
], async () => {
await net.integrationRemove();
this.section.handleRefresh();
});
},
}).render();
}
const tooltip = [];
tooltip.push(_('OpenWrt integration incomplete'));
tooltip.push('');
tooltip.push(`${_('Interface')}: ${String(hasInterface)}`);
tooltip.push(`${_('Device')}: ${String(hasDevice)}`);
if (await net.hasDnsmasq()) {
tooltip.push(`${_('Dnsmasq')}: ${String(hasDnsmasqExclusion)}`);
}
tooltip.push('');
tooltip.push(_('Click icon to repair'));
return new podmanUI.Tooltip('❌', tooltip.join('\n'), {
click: () =>
net.integrationRepair().then(() => this.section.handleRefresh()),
}).render();
}
});
@@ -0,0 +1,202 @@
'use strict';
'require dom';
'require ui';
'require podman.rpc as podmanRPC';
'require podman.utils as podmanUtil';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
'require view.podman.modal.prune as PruneModal';
'require view.podman.modal.autoUpdate as AutoUpdateModal';
/**
* Podman Overview Dashboard View
*/
return podmanView.base.extend({
async load() {
return Promise.all([
podmanRPC.system.version(),
podmanRPC.system.info(),
])
},
async render([version, info]) {
const sections = await Promise.all([
this.createToolbar(),
this.createInfoSection(version, info),
this.createDebugSection(),
this.createResourcesSection(),
]);
this.loadAdditionalInformation();
return E('div', {}, sections.filter(Boolean));
},
async loadAdditionalInformation() {
const [containers, images, volumes, networks, pods] = await Promise.all([
podmanRPC.containers.list('all=true'),
podmanRPC.images.list(),
podmanRPC.volumes.list(),
podmanRPC.networks.list(),
podmanRPC.pods.list()
]);
const runningContainers = containers.filter((container) => container.isRunning()).length;
const runningPods = pods.filter((p) => p.Status === 'Running').length;
if (this.resourceCardsEl) {
dom.content(this.resourceCardsEl, [
this.createResourceCards(containers, pods, images, networks, volumes,
runningContainers, runningPods)
]);
}
},
createToolbar() {
return E('div', { class: 'overview-actions mb-sm' }, [
new podmanUI.Button(
_('Check for Updates'),
ui.createHandlerFn(this, 'handleAutoUpdate'),
'positive'
).render(),
new podmanUI.Button(
_('Cleanup / Prune'),
ui.createHandlerFn(this, 'handlePrune'),
'remove'
).render()
]);
},
createInfoSection(version, info) {
const memTotal = podmanUtil.format.bytes(info.host?.memTotal);
const memFree = podmanUtil.format.bytes(info.host?.memFree);
const table = new podmanUI.TableList()
.addRow(_('Podman Version'), version.Version || _('Unknown'))
.addRow(_('API Version'), version.ApiVersion || _('Unknown'))
.addRow(_('CPU'), info.host?.cpus?.toString() || _('Unknown'))
.addRow(_('Memory'), memFree + ' / ' + memTotal)
.addRow(_('Socket Path'), info.host?.remoteSocket?.path || '/run/podman/podman.sock')
.addRow(_('Graph Root'), info.store?.graphRoot || _('Unknown'))
.addRow(_('Run Root'), info.store?.runRoot || _('Unknown'))
.addRow(_('Registries'), this.getRegistries(info))
;
return this.createSection(_('Information'), table.render());
},
createDebugSection() {
this.systemDiagnosticsEl = E('div', { class: 'system-diagnostics', style: 'margin-bottom: 18px;' }, [
new podmanUI.Button(
_('Load'),
ui.createHandlerFn(this, 'handleLoadSystemDiagnostics'),
'positive'
).render(),
]);
return this.createSection(_('System Diagnostics'), this.systemDiagnosticsEl);
},
createResourcesSection() {
this.resourceCardsEl = E('div', { class: 'resources-cards' }, [
E('div', { class: 'loading-placeholder' }, [
E('em', { class: 'spinning' }, _('Loading...')),
]),
]);
return this.createSection(_('Resources'), this.resourceCardsEl);
},
createSection(headline, content) {
return E('div', {}, [
E('h3', {}, headline),
content,
]);
},
getRegistries(info) {
if (info.registries && info.registries.search) {
return info.registries.search.join(', ');
}
return 'docker.io, registry.fedoraproject.org, registry.access.redhat.com';
},
createResourceCards(containers, pods, images, networks, volumes, runningContainers,
runningPods) {
return E('div', { class: 'overview-cards' }, [
this.createCard('🐳', _('Containers'), 'containers', containers.length, runningContainers, 'admin/podman/containers'),
this.createCard('🔗', _('Pods'), 'pods', pods.length, runningPods, 'admin/podman/pods'),
this.createCard('💿', _('Images'), 'images', images.length, null, 'admin/podman/images'),
this.createCard('🌐', _('Networks'), 'networks', networks.length, null, 'admin/podman/networks'),
this.createCard('💾', _('Volumes'), 'volumes', volumes.length, null, 'admin/podman/volumes'),
]);
},
createCard(icon, title, slug, total, running, url) {
const statsText = running !== null ? running + ' / ' + total : total.toString();
return E('a', {
href: L.url(url),
class: 'd-flex flex-column cursor-pointer align-center overview-card-link overview-card-' + slug,
}, [
E('div', { class: 'card-link-header d-flex align-center w-100' }, [
E('span', { class: 'card-link-title text-bold' }, title),
icon,
]),
E('div', { class: 'w-100' }, [
E('div', { class: 'card-link-headline text-bold' }, statsText),
running !== null
? E('div', { class: 'card-link-text' }, _('running') + ' / ' + _('total'))
: E('div', { class: 'card-link-text' }, _('total'))
])
]);
},
async handleLoadSystemDiagnostics() {
this.loading(_('Load System diagnostics'));
const debugData = await podmanRPC.system.debug();
const statusIcons = { ok: '\u2713', warn: '\u26A0', error: '\u2717' };
const statusColors = { ok: '#2ecc71', warn: '#e67e22', error: '#e74c3c' };
const table = new podmanUI.TableList();
(debugData.checks || []).forEach((check) => {
const icon = statusIcons[check.status] || '?';
const color = statusColors[check.status] || '#666';
const detail = check.detail || '';
const message = check.message ? ' \u2014 ' + check.message : '';
table.addRow(
_(check.label),
E('span', { style: 'color:' + color }, [
E('strong', {}, icon + ' '),
E('span', { class: 'cli-value' }, detail),
message
])
);
});
dom.content(this.systemDiagnosticsEl, [table.render()]);
ui.hideModal();
},
handlePrune() {
PruneModal.render();
},
handleAutoUpdate() {
this.loading(_('Find updateable containers'));
// @todo Use podman api filter
return podmanRPC.containers.list('all=true').then((containers) => {
AutoUpdateModal
.setContainers(containers.filter((c) => c.getAutoUpdateLabel()))
.render();
});
},
});
@@ -0,0 +1,87 @@
'use strict';
'require dom';
'require form';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
'require podman.form as podmanForm';
'require podman.model.Container as Container';
'require podman.utils as podmanUtil';
'require podman.TableSelectSection as TableSelectSection';
return podmanView.tabContent.extend({
tab: 'ps',
pod: null,
async render(pod) {
this.pod = pod;
const containers = [];
for (const podContainer of this.pod.getContainers()) {
const containerInspect = await Container.getSingleton(podContainer).inspect();
const container = Container.getSingleton(containerInspect);
containers.push(container);
}
this.map = new form.JSONMap({ containers }, '');
this.section = this.map.section(
TableSelectSection.TableSelectSection,
'containers',
'',
''
);
this.section.view = this;
this.section.anonymous = true;
this.section.footer = true;
this.section.selectKey = 'Id';
// this.section.createForm = PodmanFormContainer;
this.section.handleCreate = null;
this.section.handleRemove = null;
this.section.handleRefresh = null;
// this.section.handleRefreshCb = () => this.checkInitScripts();
// this.section.handleRefreshCb = null;
// this.section.toolbarExtraButtons = [
// new podmanUI.Button('&#9658;', ui.createHandlerFn(this, 'handleStart')).render(),
// new podmanUI.Button('&#9724;', ui.createHandlerFn(this, 'handleStop')).render(),
// new podmanUI.Button('&#8635;', ui.createHandlerFn(this, 'handleRestart')).render(),
// ];
let o;
o = this.section.option(podmanForm.field.DummyValue, 'Names', _('Name'));
o.cfgdatavalue = (container) => container.getName();
o = this.section.option(podmanForm.field.DummyValue, 'Id', _('ID'));
o.cfgdatavalue = (container) => container.getDetailLink(podmanUtil.truncate(container.getID(), 10));
o.width = '12%';
o = this.section.option(podmanForm.field.DummyValue, 'Image', _('Image'));
o.cfgdatavalue = (container) => container.getImageName();
o.width = '25%';
o = this.section.option(podmanForm.field.DummyValue, 'ImageTag', _('Tag'));
o.cfgdatavalue = (container) => container.getImageTag();
o.cfgformatter = (imageTag) => podmanUtil.truncate(imageTag, 10);
o.cfgtt = (imageTag) => imageTag.length > 10 ? imageTag : '';
o.width = '11%';
o = this.section.option(podmanForm.field.DummyValue, 'State', _('Status'));
o.cfgdatavalue = (container) => container.getStateBadge();
o.width = '8%';
o = this.section.option(podmanForm.field.DummyValue, 'StartedAt', _('Started At'));
o.cfgdatavalue = (container) => container.getStartedAt(true);
o.width = '17%';
o = this.section.option(podmanForm.field.DummyValue, 'InitScript', _('Boot'));
o.cfgvalue = () => E('span', { class: 'autostart-status autostart-' }, '...');
o.width = '6%';
return this.map.render();
},
});
@@ -0,0 +1,135 @@
'use strict';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
'require podman.utils as podmanUtil';
'require podman.model.Container as Container';
const DASH = '-';
function joinOrDash(values) {
return Array.isArray(values) && values.length > 0 ? values.join(', ') : DASH;
}
function valueOrDash(value) {
return (value === undefined || value === null || value === '') ? DASH : value;
}
return podmanView.tabContent.extend({
tab: 'info',
pod: null,
render(pod) {
this.pod = pod;
const sections = [
this.basicSection(),
this.configSection(),
this.resourcesSection(),
this.networkSection(),
];
const labels = this.labelsSection();
if (labels) sections.push(labels);
return sections;
},
basicSection() {
const table = new podmanUI.TableList();
table
.addRow(_('Name'), this.pod.getName())
.addRow(_('ID'), (this.pod.getID() || '').substring(0, 64))
.addRow(_('Status'), this.pod.getStatusBadge())
.addRow(_('Created'), podmanUtil.format.date(this.pod.getCreatedAt()));
return E('div', {}, [
E('h4', {}, _('Basic Information')),
table.render(),
]);
},
configSection() {
const table = new podmanUI.TableList();
const infraId = this.pod.getInfraId();
const infraIdCell = infraId
? E('a', { href: L.url('admin/podman/container', infraId) }, infraId.substring(0, 12))
: DASH;
table
.addRow(_('Hostname'), valueOrDash(this.pod.Hostname))
.addRow(_('Cgroup parent'), valueOrDash(this.pod.CgroupParent))
.addRow(_('Exit policy'), valueOrDash(this.pod.ExitPolicy))
.addRow(_('Restart policy'), valueOrDash(this.pod.RestartPolicy))
.addRow(_('Shared namespaces'), joinOrDash(this.pod.SharedNamespaces))
.addRow(_('Infra container'), infraIdCell);
return E('div', {}, [
E('h4', {}, _('Configuration')),
table.render(),
]);
},
resourcesSection() {
const table = new podmanUI.TableList();
const memLimit = this.pod.memory_limit;
const memLimitDisplay = memLimit > 0 ? podmanUtil.format.bytes(memLimit) : _('Unlimited');
const cpuQuota = this.pod.cpu_quota;
const cpuQuotaDisplay = (cpuQuota === undefined || cpuQuota < 0) ? _('Unlimited') : `${cpuQuota} µs`;
const cpuPeriod = this.pod.cpu_period;
const cpuPeriodDisplay = cpuPeriod > 0 ? `${cpuPeriod} µs` : DASH;
table
.addRow(_('CPU period'), cpuPeriodDisplay)
.addRow(_('CPU quota'), cpuQuotaDisplay)
.addRow(_('CPU shares'), valueOrDash(this.pod.cpu_shares))
.addRow(_('CPU set'), valueOrDash(this.pod.cpuset_cpus))
.addRow(_('Memory limit'), memLimitDisplay);
return E('div', {}, [
E('h4', {}, _('Resources')),
table.render(),
]);
},
networkSection() {
const table = new podmanUI.TableList();
const infra = this.pod.InfraConfig || {};
table
.addRow(_('Networks'), joinOrDash(infra.Networks || this.pod.getNetworks()))
.addRow(_('Static IPv4'), valueOrDash(infra.StaticIP))
.addRow(_('Static MAC'), valueOrDash(infra.StaticMAC))
.addRow(_('DNS servers'), joinOrDash(infra.DNSServer))
.addRow(_('DNS search'), joinOrDash(infra.DNSSearch))
.addRow(_('DNS options'), joinOrDash(infra.DNSOption))
.addRow(_('Extra hosts'), joinOrDash(infra.HostAdd))
.addRow(_('Host network'), infra.HostNetwork ? _('Yes') : _('No'));
return E('div', {}, [
E('h4', {}, _('Network')),
table.render(),
]);
},
labelsSection() {
const labels = this.pod.getLabels();
const keys = Object.keys(labels || {});
if (keys.length === 0) return null;
const table = new podmanUI.TableList();
for (const key of keys) {
table.addRow(key, labels[key]);
}
return E('div', {}, [
E('h4', {}, _('Labels')),
table.render(),
]);
},
});
@@ -0,0 +1,96 @@
'use strict';
'require dom';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
return podmanView.tabContent.extend({
tab: 'ps',
pod: null,
render(pod) {
this.pod = pod;
if (!this.pod.isRunning()) {
return this.warningContent(_('Pod is not running'));
}
this.tableContent = E('div', { class: 'ps-table-content' }, []);
return this.renderTabContent('', [ this.tableContent ]);
},
onTabActive() {
if (!this.pod || !this.pod.isRunning() || this.processStream) return;
this.processStream = this.pod.streamTop((ps) => {
this.updateProcessList(ps);
});
},
onTabInactive() {
if (!this.processStream) return;
this.processStream.stop();
this.processStream = null;
},
updateProcessList(ps) {
if (!ps || !ps.Titles || !ps.Processes || ps.Titles.length === 0) {
return;
}
const psTable = new podmanUI.Table();
const columnWidth = `width: ${100 / ps.Titles.length}%;`;
const timeIndex = ps.Titles.indexOf('ELAPSED');
ps.Titles.forEach((title) => {
psTable.addHeader(_(title), { style: columnWidth });
});
ps.Processes.forEach((process) => {
psTable.addRow(process.map((detail, i) => ({
inner: i === timeIndex ? this._formatElapsedTime(detail) : detail
})));
});
dom.content(this.tableContent, psTable.render());
},
// @todo: duplicated code (containers-tab/processes.js)
_formatElapsedTime(timeStr) {
if (!timeStr) return '-';
const result = [];
const pattern = /(\d+(?:\.\d+)?)([ydhms])/g;
let match;
while ((match = pattern.exec(timeStr)) !== null) {
let value = parseFloat(match[1]);
const unit = match[2];
if (unit === 's' && value > 1000) {
value = Math.floor(value / 1000000000);
if (value >= 60) {
const mins = Math.floor(value / 60);
const secs = value % 60;
if (mins > 0) result.push(`${mins}m`);
if (secs > 0) result.push(`${secs}s`);
continue;
}
} else {
value = Math.floor(value);
}
if (unit === 's' || unit === 'm') {
result.push(String(value).padStart(2, '0') + unit);
} else {
result.push(value + unit);
}
}
return result.length > 0 ? result.join('') : timeStr;
}
});
@@ -0,0 +1,83 @@
'use strict';
'require dom';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
'require podman.utils as podmanUtil';
return podmanView.tabContent.extend({
tab: 'stats',
pod: null,
render(pod) {
this.pod = pod;
if (!this.pod.isRunning()) {
return this.warningContent(_('Pod is not running'));
}
this.containerNames = this.buildContainerNameMap();
this.tableContent = E('div', { class: 'stats-table-content' }, []);
return this.renderTabContent('', [ this.tableContent ]);
},
onTabActive() {
if (!this.pod || !this.pod.isRunning() || this.statsStream) return;
this.statsStream = this.pod.streamStats((reports) => {
this.updateStats(reports);
});
},
onTabInactive() {
if (!this.statsStream) return;
this.statsStream.stop();
this.statsStream = null;
},
buildContainerNameMap() {
const map = {};
for (const c of (this.pod.getContainers() || [])) {
if (c?.Id) map[c.Id] = c.Name;
}
return map;
},
updateStats(reports) {
if (!Array.isArray(reports)) return;
const table = new podmanUI.Table();
const headers = [
_('Container'),
_('CID'),
_('CPU %'),
_('Memory'),
_('Memory %'),
_('Net I/O'),
_('Block I/O'),
_('PIDs'),
];
const columnWidth = `width: ${100 / headers.length}%;`;
headers.forEach((h) => table.addHeader(h, { style: columnWidth }));
for (const r of reports) {
const cid = r.CID || '';
const name = this.containerNames[cid] || r.Name || '-';
table.addRow([
{ inner: name },
{ inner: podmanUtil.truncate(cid, 12) },
{ inner: r.CPU || '-' },
{ inner: r.MemUsage || '-' },
{ inner: r.Mem || '-' },
{ inner: r.NetIO || '-' },
{ inner: r.BlockIO || '-' },
{ inner: r.PIDS || '-' },
]);
}
dom.content(this.tableContent, table.render());
},
});
@@ -0,0 +1,150 @@
'use strict';
'require ui';
'require dom';
'require podman.ui as podmanUI';
'require podman.view as podmanView';
'require podman.model.Pod as Pod';
'require view.podman.pod-tab.info as PodInfoTab';
'require view.podman.pod-tab.containers as PodContainersTab';
'require view.podman.pod-tab.stats as PodStatsTab';
'require view.podman.pod-tab.processes as PodProcessesTab';
return podmanView.container.extend({
pod: null,
data: null,
listUrl: L.url('admin/podman/pods'),
async load() {
const path = L.location();
const matches = path.match(/pod\/([a-f0-9]+)/i);
if (!matches || !matches[1]) {
this.redirectToList();
return;
}
const podId = matches[1];
const pod = Pod.getSingleton({ Id: podId });
const inspectData = await pod.inspect();
this.data = inspectData;
return Pod.getSingleton(inspectData);
},
async render(pod) {
if (!pod) {
this.redirectToList();
return;
}
this.pod = pod;
this.tabs
.addTab('info', _('Info'))
.addTab('containers', _('Containers'))
.addTab('stats', _('Stats'))
.addTab('ps', _('Processes'))
.addTab('inspect', _('Inspect'));
requestAnimationFrame(() => {
this.renderInfoTab();
this.renderContainersTab();
this.renderStatsTab();
this.renderProcessesTab();
this.renderInspectTab();
});
return this.super('render', []);
},
createHeader() {
return this.super('createHeader', [
this.pod.getName(),
this.pod.isRunning(),
this.pod.isStopped(),
this.pod.isPaused(),
]);
},
stopStreams() {
this.getTabInstance('stats')?.onTabInactive();
this.getTabInstance('ps')?.onTabInactive();
},
async renderInfoTab() {
const content = await PodInfoTab.render(this.pod);
this.renderTab('info', content);
},
async renderContainersTab() {
const content = await PodContainersTab.render(this.pod);
this.renderTab('containers', content);
},
async renderStatsTab() {
const content = await PodStatsTab.render(this.pod);
this.renderTab('stats', content);
},
async renderProcessesTab() {
const content = await PodProcessesTab.render(this.pod);
this.renderTab('ps', content);
},
renderInspectTab() {
this.renderTab('inspect', new podmanUI.JsonArea(this.data).render());
},
async handleStart() {
if (this.pod.isRunning()) {
return;
}
this.loading(_('Start pod'));
if (this.pod.isPaused()) {
this.pod.unpause().then(() => window.location.reload());
return;
}
this.pod.start().then(() => window.location.reload());
},
async handleStop() {
if (!this.pod.isRunning() && !this.pod.isPaused()) {
return;
}
this.loading(_('Stop pod'));
this.stopStreams();
this.pod.stop().then(() => window.location.reload());
},
async handleRestart() {
this.loading(_('Restart pod'));
this.stopStreams();
this.pod.restart().then(() => window.location.reload());
},
async handlePause() {
if (!this.pod.isRunning()) {
return;
}
this.loading(_('Pause pod'));
this.stopStreams();
this.pod.pause().then(() => window.location.reload());
},
async handleRemove() {
this.confirm([
E('p', {}, _('Are you sure to remove pod?')),
], async () => {
this.loading(_('Remove pod'));
this.pod.remove().then(() => this.redirectToList());
});
},
});
@@ -0,0 +1,431 @@
:root {
--podman-terminal-bg: #1a1a1a;
--podman-terminal-fg: #0f0;
--warn-background-color-dark: rgba(166, 141, 69, 0.2);
}
.overview-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1.3125rem;
}
div.cbi-tooltip-container {
width: fit-content;
}
div.cbi-tooltip-container div.cbi-tooltip {
font-size: 13px;
font-weight: 400;
line-height: normal;
}
.overview-card-link,
.overview-card-link:hover,
.overview-card-link:focus {
text-decoration: none;
color: inherit;
justify-content: space-between;
min-height: 120px;
padding: 1.3125rem;
margin: 0 0 .625rem 0;
transition: all 0.2s ease-in-out;
}
.overview-card-link .card-link-header {
justify-content: space-between;
margin-bottom: .625rem;
font-size: 20px;
}
.overview-card-link .card-link-title {
font-size: 16px;
opacity: 0.8;
}
.overview-card-link .card-link-icon {
font-size: 24px;
opacity: 0.6;
}
.overview-card-link .card-link-headline {
font-size: 32px;
}
.overview-card-link .card-link-text {
font-size: 12px;
opacity: 0.7;
margin-top: .3125rem;
}
.overview-card-containers {
border-left: 4px solid #3498db;
}
.overview-card-containers .card-link-headline {
color: #3498db;
}
.overview-card-pods {
border-left: 4px solid #2ecc71;
}
.overview-card-pods .card-link-headline {
color: #2ecc71;
}
.overview-card-images {
border-left: 4px solid #9b59b6;
}
.overview-card-images .card-link-headline {
color: #9b59b6;
}
.overview-card-networks {
border-left: 4px solid #e67e22;
}
.overview-card-networks .card-link-headline {
color: #e67e22;
}
.overview-card-volumes {
border-left: 4px solid #34495e;
}
.overview-card-volumes .card-link-headline {
color: #34495e;
}
.border-0 {
border: 0 !important;
}
.cursor-pointer {
cursor: pointer;
}
.d-flex {
display: flex;
}
.align-center {
align-items: center;
}
.align-start {
align-items: start;
}
.justify-center {
justify-content: center;
}
.justify-end {
justify-content: end;
}
.flex-column {
flex-direction: column;
}
.gap-5 {
gap: .3125rem;
}
.cbi-button-active {
background: linear-gradient(lightgray, lightgray 25%, var(--background-color-low));
}
.badge {
font-size: 0.85em;
padding: 0 4px;
text-align: center;
border-radius: .3125rem;
width: fit-content;
}
.badge.running {
background-color: var(--success-color-high);
}
.badge.exited {
background-color: var(--error-color-high);
}
.badge.created {
background-color: var(--background-color-low);
}
.badge.paused {
background-color: var(--warning-color-high, #d68a00);
}
.badge.degraded {
background-color: var(--warning-color-high, #d68a00);
}
.badge.stopped,
.badge.dead {
background-color: var(--error-color-high);
}
.badge.running,
.badge.error,
.badge.exited,
.badge.paused,
.badge.degraded,
.badge.stopped,
.badge.dead {
color: white;
}
.tooltip {
border-bottom: 1px dotted;
}
.w-100 {
width: 100%;
}
.d-none {
display: none;
}
.modal.loading-modal {
padding: 1em;
}
.cbi-checkbox + label {
margin-left: 3px;
}
.text-success {
color: var(--success-color-high);
}
.text-error {
color: var(--error-color-high);
}
.mr-xs {
margin-right: .3125rem;
}
.ml-xs {
margin-left: .3125rem;
}
.ml-sm {
margin-left: .625rem;
}
.mt-sm {
margin-top: .625rem;
}
.mb-xs {
margin-bottom: .3125rem;
}
.mb-sm {
margin-bottom: .625rem;
}
.p-sm {
padding: .625rem;
}
.text-bold {
font-weight: bold;
}
.text-center {
text-align: center !important;
}
.d-flex {
display: flex;
}
.gap-xs {
gap: .25rem;
}
.gap-sm {
gap: .625rem;
}
.editable-field {
width: 100%;
display: flex;
}
.table-list>tr>td:first-of-type {
width: 33%;
font-weight: bold;
}
.code-area,
.json-area,
.terminal-area {
max-height: 600px;
font-weight: 100;
overflow: auto !important;
white-space: pre !important;
display: block;
word-wrap: normal;
}
.json-area .key {
color: #660066;
font-weight: bold;
}
.json-area .string {
color: #007700;
}
.json-area .boolean {
color: #aa5500;
}
.json-area .null {
color: #777777;
}
.json-area .number {
color: #0000ff;
}
.code-area.bash .comment {
color: #888888;
font-style: italic;
}
.code-area.bash .string {
color: #007700;
}
.code-area.bash .variable {
color: #aa5500;
}
.code-area.bash .keyword {
color: #0000ff;
font-weight: bold;
}
.code-area.bash .number {
color: #ff4500;
}
.terminal-area {
min-height: 100px;
background: var(--podman-terminal-bg);
color: var(--podman-terminal-fg);
}
.autostart-enabled {
color: var(--success-color-high);
}
.autostart-none,
.autostart-disabled {
color: var(--text-color-medium);
}
.autostart-missing {
color: var(--warn-color-high);
cursor: pointer;
}
.autostart-error {
color: var(--error-color-high);
}
.loading-placeholder {
text-align: center;
padding: 30px;
}
.cbi-button-none {
border: 0;
background: transparent;
box-shadow: none;
}
.container-toolbar .d-flex {
gap: .25rem;
}
.checkbox-with-label .cbi-checkbox {
display: flex;
align-items: center;
}
.container-icon-disconnect {
border: 0;
}
#log-viewer {
height: 600px;
overflow: auto;
white-space: pre;
}
#log-lines {
width: 50px;
}
#log-since,
#log-until {
width: 100px;
}
#log-lines input,
#log-since input,
#log-until input {
width: inherit;
}
.cbi-section-table-select {
table-layout: fixed;
}
th[data-widget="Podman.Form.SelectDummyValue"] {
padding: .625rem .625rem 8px !important;
}
th[data-widget="Podman.Form.SelectDummyValue"] .cbi-checkbox {
margin-top: 1px;
}
td[data-widget="Podman.Form.SelectDummyValue"] {
padding: .625rem .625rem 7px !important;
}
td[data-widget="Podman.Form.SelectDummyValue"] .cbi-checkbox {
margin-top: 2px;
}
.podman-stream-log {
font-family: monospace;
font-size: 0.85rem;
line-height: 1.35;
background: #1e1e1e;
color: #e8e8e8;
border: 1px solid #444;
border-radius: 3px;
padding: 0.5rem 0.75rem;
max-height: 320px;
overflow-y: auto;
overflow-x: auto;
white-space: pre;
}
.podman-stream-log .log-line {
white-space: pre;
word-break: keep-all;
}
@@ -0,0 +1,172 @@
'use strict';
'require ui';
'require podman.rpc as podmanRPC';
'require podman.utils as utils';
'require podman.ui as podmanUI';
'require podman.form as podmanForm';
'require podman.view as podmanView';
'require podman.form.pod as PodmanFormPod';
'require podman.model.Container as Container';
return podmanView.list.extend({
sectionName: 'pods',
title: _('Pods'),
titleSingle: _('Pod'),
async load() {
return podmanRPC.pods.list();
},
async updateSection() {
this.section.selectKey = 'Id';
this.section.createForm = PodmanFormPod;
this.section.toolbarExtraButtons = [
new podmanUI.Button('&#9658;', ui.createHandlerFn(this, 'handleStart')).render(),
new podmanUI.Button('&#9724;', ui.createHandlerFn(this, 'handleStop')).render(),
new podmanUI.Button('&#8635;', ui.createHandlerFn(this, 'handleRestart')).render(),
new podmanUI.Button('&#10074;&#10074;', ui.createHandlerFn(this, 'handlePause')).render(),
];
let o;
o = this.section.option(podmanForm.field.DummyValue, 'Name', _('Name'));
o = this.section.option(podmanForm.field.DummyValue, 'Id', _('ID'));
o.cfgdatavalue = (pod) => pod.getDetailLink(utils.truncate(pod.getID(), 16));
o.width = '20%';
o = this.section.option(podmanForm.field.DummyValue, 'Status', _('Status'));
o.cfgdatavalue = (pod) => pod.getStatusBadge();
o.width = '10%';
o = this.section.option(podmanForm.field.DummyValue, 'Containers', _('Containers'));
o.cfgdatavalue = (pod) => this.renderContainersCell(pod);
o.width = '12%';
o = this.section.option(podmanForm.field.DummyValue, 'Networks', _('Networks'));
o.cfgdatavalue = (pod) => {
const nets = pod.getNetworks();
return nets.length > 0 ? nets.join(', ') : '-';
};
o.width = '15%';
o = this.section.option(podmanForm.field.DateDummyValue, 'Created', _('Created'));
o.width = '20%';
},
renderContainersCell(pod) {
const count = pod.getContainers().length;
if (count === 0) {
return E('span', {}, '0');
}
return E('a', {
href: '#',
class: 'text-bold',
click: (ev) => {
ev.preventDefault();
this.handleShowContainers(pod);
}
}, `${count} ▸`);
},
handleShowContainers(pod) {
const containers = pod.getContainers();
const headerRow = E('tr', {}, [
E('th', {}, _('Name')),
E('th', {}, _('ID')),
E('th', {}, _('Status')),
]);
const rows = containers.map((c) => {
const container = Container.getSingleton(c);
const idShort = utils.truncate(container.getID(), 10);
const idCell = container.getDetailLink(idShort);
return E('tr', {}, [
E('td', {}, container.getName()),
E('td', {}, idCell),
E('td', {}, container.getStateBadge()),
]);
});
// @todo: create table with helpers or show a simpler list
const table = E('table', {
class: 'table cbi-section-table'
}, [
E('thead', {}, headerRow),
E('tbody', {}, rows.length > 0 ? rows : [
E('tr', {}, E('td', {
colspan: 3,
class: 'text-center'
}, _('No containers in this pod'))),
]),
]);
const title = _('Containers in pod %s').format(pod.getName());
const modal = new podmanUI.Modal(title, [table]);
modal.render();
},
handleStart() {
return this.runOnSelected(
(pod) => pod.isPaused() ? pod.unpause() : pod.start(),
_('Starting pod')
);
},
handleStop() {
return this.runOnSelected((pod) => pod.stop(), _('Stopping pod'));
},
handleRestart() {
return this.runOnSelected((pod) => pod.restart(), _('Restarting pod'));
},
handlePause() {
return this.runOnSelected((pod) => pod.pause(), _('Pausing pod'));
},
async runOnSelected(action, textLoad) {
const section = this.getSection();
const selected = section.getSelectedData();
if (selected.length === 0) {
return section.showNoneSelectedWarning();
}
const failures = [];
for (const [i, pod] of selected.entries()) {
this.loading(_('%s: %d/%d').format(textLoad, i + 1, selected.length));
const result = await action(pod);
if (result?.Errs?.length > 0) {
failures.push({
name: pod.getName(),
errs: result.Errs
});
}
}
ui.hideModal();
if (failures.length > 0) {
const blocks = failures.map(({
name,
errs
}) => E('div', {
class: 'mb-sm'
}, [
E('strong', {}, name + ':'),
E('ul', {
class: 'm-0'
}, errs.map((e) => E('li', {}, e))),
]));
podmanUI.alert(blocks, 'error');
}
section.handleRefresh();
},
});
@@ -0,0 +1,59 @@
'use strict';
'require form';
'require podman.utils as utils';
'require podman.rpc as podmanRPC';
'require podman.form as podmanForm';
'require podman.view as podmanView';
'require podman.form.secret as PodmanFormSecret';
const SECRET_LABELS_TRUNCATE_LEN = 30;
/**
* Manage podman secrets
*/
return podmanView.list.extend({
sectionName: 'secrets',
title: _('Secrets'),
titleSingle: _('Secret'),
/**
* Load secret data
*/
async load() {
return podmanRPC.secrets.list();
},
/**
* Update list section
*/
async updateSection() {
this.section.selectKey = 'ID';
this.section.createForm = PodmanFormSecret;
let o;
o = this.section.option(podmanForm.field.LinkDummyValue, 'Name', _('Name'));
o.cfgdatavalue = (secret) => secret.getName();
o.click = (_value, secret) => this.section.handleInspect(secret, ['SecretData']);
o = this.section.option(podmanForm.field.DummyValue, 'Driver', _('Driver'));
o.cfgdatavalue = (secret) => secret.getDriver();
o.width = '10%';
o = this.section.option(podmanForm.field.DummyValue, 'Labels', _('Labels'));
o.cfgdatavalue = (secret) => {
const labels = secret.getLabels();
const keys = Object.keys(labels);
return keys.length ? keys.map((key) => `${key}=${labels[key]}`).join(', ') : '-';
};
o.cfgformatter = (labels) => utils.truncate(labels, SECRET_LABELS_TRUNCATE_LEN);
o.cfgtt = (labels) => labels.length > SECRET_LABELS_TRUNCATE_LEN ? labels : '';
o.width = '20%';
o = this.section.option(podmanForm.field.DateDummyValue, 'CreatedAt', _('Created'));
o.width = '20%';
},
});
@@ -0,0 +1,122 @@
'use strict';
'require ui';
'require fs';
'require podman.utils as utils';
'require podman.rpc as podmanRPC';
'require podman.ui as podmanUI';
'require podman.form as podmanForm';
'require podman.view as podmanView';
'require podman.form.volume as PodmanFormVolume';
'require podman.form.volumeImport as PodmanFormVolumeImport';
const VOLUME_MOUNT_TRUNCATE_LEN = 30;
return podmanView.list.extend({
sectionName: 'volumes',
title: _('Volumes'),
titleSingle: _('Volume'),
async load() {
return podmanRPC.volumes.list();
},
async updateSection() {
this.section.selectKey = 'Name';
this.section.createForm = PodmanFormVolume;
this.section.toolbarExtraButtons = [
new podmanUI.ButtonNew(_('Import'), {
click: ui.createHandlerFn(this, 'handleImport'),
}).render(),
new podmanUI.ButtonNew(_('Export'), {
click: ui.createHandlerFn(this, 'handleExport'),
}).render(),
];
let o;
o = this.section.option(podmanForm.field.LinkDummyValue, 'VolumeName', _('Name'));
o.cfgdatavalue = (volume) => utils.truncate(volume.getName(), 20);
o.cfgtt = (_cfg, volume) => volume.getName().length > 20 ? volume.getName() : '';
o.click = (_cfg, volume) => this.section.handleInspect(volume);
o = this.section.option(podmanForm.field.DummyValue, 'Driver', _('Driver'));
o.width = '10%';
o = this.section.option(podmanForm.field.DummyValue, 'Mountpoint', _('Mountpoint'));
o.cfgformatter = (mountpoint) => utils.truncate(mountpoint, VOLUME_MOUNT_TRUNCATE_LEN);
o.cfgtt = (mountpoint) => mountpoint.length > VOLUME_MOUNT_TRUNCATE_LEN ? mountpoint : '';
o.width = '35%';
o = this.section.option(podmanForm.field.DateDummyValue, 'CreatedAt', _('Created'));
o.width = '20%';
},
handleImport() {
const fileInput = E('input', {
type: 'file',
accept: '.tar,.tar.gz,.tgz',
class: 'hidden'
});
fileInput.addEventListener('change', async (ev) => {
document.body.removeChild(fileInput);
const file = ev.target.files[0];
if (!file) return;
const importForm = new PodmanFormVolumeImport.init();
const formContent = await importForm.render(file);
const title = _('Import from archive: %s').format(file.name);
const modal = new podmanUI.Modal(title, [ formContent ]);
modal.getButtons = () => [
modal.getCloseButton(),
new podmanUI.ButtonNew(_('Import'), {
click: async () => {
await importForm.handleCreate();
await this.section.handleRefresh();
},
type: 'positive',
}).render(),
];
modal.render();
});
document.body.appendChild(fileInput);
fileInput.click();
},
async handleExport() {
const selected = this.section.getSelectedData();
if (selected.length === 0) {
return this.section.showNoneSelectedWarning();
}
let errors = 0;
for (const [index, volume] of selected.entries()) {
this.loading(_('Exporting selected volumes: %d/%d').format(index + 1, selected.length));
const volumeName = volume.getName();
try {
const blob = await fs.exec_direct('/usr/libexec/podman-api', ['volume_export', volumeName], 'blob');
const url = URL.createObjectURL(blob);
const a = E('a', { href: url, download: `${volumeName}.tar` });
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(() => URL.revokeObjectURL(url), 100);
} catch (err) {
podmanUI.alert(_('Error: %s').format(err.message), 'error');
errors++;
}
}
ui.hideModal();
if (errors === 0)
this.success(_('All volumes exported successfully'));
},
});