Files
2026-02-22 11:42:17 +08:00

357 lines
10 KiB
JavaScript

'use strict';
'require form';
'require fs';
'require dockerman.common as dm2';
/*
Copyright 2026
Docker manager JS for Luci by Paul Donald <newtwen+github@gmail.com>
LICENSE: GPLv2.0
*/
/* API v1.52
GET /events supports content-type negotiation and can produce either
application/x-ndjson (Newline delimited JSON object stream) or
application/json-seq (RFC7464).
application/x-ndjson:
{"some":"thing\n"}
{"some2":"thing2\n"}
...
application/json-seq: ␊ = \n | ^J | 0xa, ␞ = ␞ | ^^ | 0x1e
␞{"some":"thing\n"}␊
␞{"some2":"thing2\n"}␊
...
*/
return dm2.dv.extend({
load() {
const now = Math.floor(Date.now() / 1000);
this.js_api = false;
return Promise.all([
dm2.docker_events({ query: { since: `0`, until: `${now}` } }),
dm2.js_api_ready.then(([ok, ]) => this.js_api = ok),
]);
},
render([events, js_api_available]) {
if (events?.code !== 200) {
return E('div', {}, [ events?.body?.message ]);
}
this.outputText = events?.body ? JSON.stringify(events?.body, null, 2) + '\n' : '';
const event_list = events?.body || [];
const view = this;
const mainContainer = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, [_('Docker - Events')])
]);
// Filters
const now = new Date();
const nowIso = now.toISOString().slice(0, 16);
const filtersSection = E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-node' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Type')),
E('div', { 'class': 'cbi-value-field' }, [
E('select', {
'id': 'event-type-filter',
'class': 'cbi-input-select',
'change': () => {
view.updateSubtypeFilter(this.value);
view.renderEventsTable(event_list);
}
}, [
E('option', { 'value': '' }, _('All Types')),
...Object.keys(dm2.Types).map(type =>
E('option', { 'value': type }, `${dm2.Types[type].i18n}`)
)
])
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('Subtype')),
E('div', { 'class': 'cbi-value-field' }, [
E('select', {
'id': 'event-subtype-filter',
'class': 'cbi-input-select',
'disabled': true,
'change': () => {
view.renderEventsTable(event_list);
}
}, [
E('option', { 'value': '' }, _('Select Type First'))
])
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('From')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'id': 'event-from-date',
'type': 'datetime-local',
'value': '1970-01-01T00:00',
'step': 60,
'style': 'width: 180px;',
'change': () => { view.renderEventsTable(event_list); }
}),
E('button', {
'type': 'button',
'class': 'cbi-button',
'style': 'margin-left: 8px;',
'click': () => {
const now = new Date();
const iso = now.toISOString().slice(0,16);
document.getElementById('event-from-date').value = iso;
view.renderEventsTable(event_list);
}
}, _('Now')),
E('button', {
'type': 'button',
'class': 'cbi-button',
'style': 'margin-left: 8px;',
'click': () => {
const unixzero = new Date(0);
const iso = unixzero.toISOString().slice(0,16);
document.getElementById('event-from-date').value = iso;
view.renderEventsTable(event_list);
}
}, _('0'))
])
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, _('To')),
E('div', { 'class': 'cbi-value-field' }, [
E('input', {
'id': 'event-to-date',
'type': 'datetime-local',
'value': nowIso,
'step': 60,
'style': 'width: 180px;',
'change': () => { view.renderEventsTable(event_list); }
}),
E('button', {
'type': 'button',
'class': 'cbi-button',
'style': 'margin-left: 8px;',
'click': () => {
const now = new Date();
const iso = now.toISOString().slice(0,16);
document.getElementById('event-to-date').value = iso;
view.renderEventsTable(event_list);
}
}, _('Now'))
])
])
])
]);
mainContainer.appendChild(filtersSection);
this.tableSection = E('div', { 'class': 'cbi-section', 'id': 'events-section' });
mainContainer.appendChild(this.tableSection);
this.renderEventsTable(event_list);
mainContainer.appendChild(this.insertOutputFrame(E('div', {}), null));
return mainContainer;
},
renderEventsTable(event_list) {
const view = this;
// Get filter values
const typeFilter = document.getElementById('event-type-filter')?.value || '';
const subtypeFilter = document.getElementById('event-subtype-filter')?.value || '';
// Build filters object for docker_events API
const filters = {};
if (typeFilter) {
filters.type = [typeFilter];
}
if (subtypeFilter) {
filters.event = [subtypeFilter];
}
// Show loading indicator
this.tableSection.innerHTML = '';
// Query docker events with filters and date range
const fromInput = document.getElementById('event-from-date');
const toInput = document.getElementById('event-to-date');
let since = '0';
let until = Math.floor(Date.now() / 1000).toString();
if (fromInput && fromInput.value) {
const fromDate = new Date(fromInput.value);
if (!isNaN(fromDate.getTime())) {
since = Math.floor(fromDate.getTime() / 1000).toString();
since = since < 0 ? 0 : since;
}
}
if (toInput && toInput.value) {
const toDate = new Date(toInput.value);
if (!isNaN(toDate.getTime())) {
const now = Date.now() / 1000;
until = Math.floor(toDate.getTime() / 1000).toString();
until = !this.js_api ? until > now ? now : until : until;
}
}
const queryParams = { since, until };
if (Object.keys(filters).length > 0) {
// docker pre v27: filters => docker *streams* events. v27, send events in body.
// Some older dockerd endpoints don't like encoded filter params, even if we can't stream.
queryParams.filters = JSON.stringify(filters);
}
event_list = new Set();
view.outputText = '';
let eventsTable = null;
// Batching for speed
let batchBuffer = new Set();
let batchTimer = null;
const BATCH_SIZE = 256;
const BATCH_INTERVAL = 500; // ms
function updateTable() {
const ev_array = Array.from(event_list.keys());
const rows = ev_array.map(event => {
const type = event.Type;
const typeInfo = dm2.Types[type];
const typeDisplay = typeInfo ? `${typeInfo.i18n}` : type;
const actionParts = event.Action?.split(':') || [];
const action = actionParts.length > 0 ? actionParts[0] : '';
const action_sub = actionParts.length > 1 ? actionParts[1] : null;
const actionInfo = typeInfo?.sub?.[action];
const actionDisplay = actionInfo ? `${actionInfo.i18n}${action_sub ? ':'+action_sub : ''}` : action;
return [
view.buildTimeString(event.time),
typeDisplay,
actionDisplay,
view.objectToText(event.Actor),
event.scope || ''
];
});
const output = JSON.stringify(ev_array, null, 2);
view.outputText = output + '\n';
view.insertOutput(view.outputText);
if (!eventsTable) {
eventsTable = new L.ui.Table(
[_('Time'), _('Type'), _('Action'), _('Actor'), _('Scope')],
{ id: 'events-table', style: 'width: 100%; table-layout: auto;' },
E('em', [_('No events found')])
);
view.tableSection.innerHTML = '';
view.tableSection.appendChild(eventsTable.render());
}
eventsTable.update(rows);
}
view.tableSection.innerHTML = '';
function flushBatch() {
if (batchBuffer.size) {
batchBuffer = new Set();
}
if (batchTimer) {
clearTimeout(batchTimer);
batchTimer = null;
}
updateTable();
}
function handleEventChunk(event) {
event_list.add(event);
batchBuffer.add(event);
if (batchBuffer.size >= BATCH_SIZE) {
flushBatch();
} else if (!batchTimer) {
batchTimer = setTimeout(flushBatch, BATCH_INTERVAL);
}
}
/* Partial transfers work but XHR times out waiting, even with xhr.timeout = 0 */
// view.handleXHRTransfer({
// q_params:{ query: queryParams },
// commandCPath: '/docker/events',
// commandDPath: '/events',
// commandTitle: dm2.ActionTypes['prune'].i18n,
// showProgress: false,
// onUpdate: (msg) => {
// try {
// if(msg.error)
// ui.addTimeLimitedNotification(dm2.ActionTypes['prune'].i18n, msg.error, 7000, 'error');
// event_list.add(msg);
// updateTable();
// const output = JSON.stringify(msg, null, 2) + '\n';
// view.insertOutput(output);
// } catch {
// }
// },
// noFileUpload: true,
// });
view.executeDockerAction(
dm2.docker_events,
{ query: queryParams, onChunk: handleEventChunk },
_('Load Events'),
{
showOutput: false,
showSuccess: false,
onSuccess: (response) => {
if (response.body)
event_list = Array.isArray(response.body) ? new Set(response.body) : new Set([response.body]);
updateTable();
flushBatch();
},
onError: (err) => {
view.tableSection.innerHTML = '';
view.tableSection.appendChild(E('em', { 'style': 'color: red;' }, _('Failed to load events: %s').format(err?.message || err)));
}
}
);
},
updateSubtypeFilter(selectedType) {
const subtypeSelect = document.getElementById('event-subtype-filter');
if (!subtypeSelect) return;
// Clear existing options
subtypeSelect.innerHTML = '';
if (!selectedType || !dm2.Types[selectedType] || !dm2.Types[selectedType].sub) {
subtypeSelect.disabled = true;
subtypeSelect.appendChild(E('option', { 'value': '' }, _('Select Type First')));
return;
}
// Enable and populate with subtypes
subtypeSelect.disabled = false;
subtypeSelect.appendChild(E('option', { 'value': '' }, _('All Subtypes')));
const subtypes = dm2.Types[selectedType].sub;
for (const action in subtypes) {
subtypeSelect.appendChild(
E('option', { 'value': action }, `${subtypes[action].i18n}`)
);
}
},
handleSave: null,
handleSaveApply: null,
handleReset: null,
});