'use strict'; 'require form'; 'require fs'; 'require dockerman.common as dm2'; /* Copyright 2026 Docker manager JS for Luci by Paul Donald 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, });