fb4454678a
Signed-off-by: sbwml <admin@cooluc.com>
357 lines
10 KiB
JavaScript
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].e} ${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.e} ${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.e} ${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].e} ${subtypes[action].i18n}`)
|
|
);
|
|
}
|
|
},
|
|
|
|
handleSave: null,
|
|
handleSaveApply: null,
|
|
handleReset: null,
|
|
|
|
});
|