mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 12:24:33 +08:00
307 lines
7.9 KiB
JavaScript
307 lines
7.9 KiB
JavaScript
'use strict';
|
|
/* SPDX-License-Identifier: Apache-2.0 */
|
|
/* Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com> */
|
|
'require baseclass';
|
|
'require fwlive.log as log';
|
|
'require fwlive.links as links';
|
|
|
|
/**
|
|
* Table thead/rows DOM renderer for luci-app-fwlive.
|
|
*
|
|
* renderThead(host, state, callbacks) → void
|
|
* host - <table id="fwlive-table"> (colgroup + thead tr cleared/rebuilt)
|
|
* state - shallow copy: { columns: [...] }
|
|
* callbacks - {} (unused; present for API consistency)
|
|
*
|
|
* renderRows(host, state, callbacks) → void
|
|
* host - <tbody> element (cleared and rebuilt; element itself is kept)
|
|
* state - shallow copy: { rows, columns, viewMode, messageLayout,
|
|
* expandedRowId, rowTint, showHostnames,
|
|
* hostnameCache, firewallBackend }
|
|
* callbacks - { onRowClick(rowId, ev), onFilterClick(field, value, ev),
|
|
* actionRowTintClass(action) }
|
|
*
|
|
* Internals (not a second public contract): columnLabel, columnCellClass,
|
|
* flowCell, buildColumnCell.
|
|
*
|
|
* Modules must not mutate state. host contents are cleared then rebuilt
|
|
* (idempotent replace). Does not touch #fwlive-scroll or #fwlive-empty.
|
|
*/
|
|
|
|
function columnLabel(col) {
|
|
const labels = {
|
|
time: _('Time'),
|
|
action: _('Action'),
|
|
rule: _('Rule'),
|
|
iface: _('Interface'),
|
|
iface_in: _('IN'),
|
|
iface_out: _('OUT'),
|
|
dir: _('Dir'),
|
|
proto: _('Proto'),
|
|
src: _('Source'),
|
|
dst: _('Destination'),
|
|
sport: _('SPort'),
|
|
dport: _('DPort'),
|
|
flags: _('Flags'),
|
|
len: _('Len'),
|
|
flow: _('Flow'),
|
|
message: _('Message')
|
|
};
|
|
|
|
return labels[col] || col;
|
|
}
|
|
|
|
function columnCellClass(col) {
|
|
switch (col) {
|
|
case 'time':
|
|
return 'fwlive-time';
|
|
case 'action':
|
|
return 'fwlive-action';
|
|
case 'rule':
|
|
return 'fwlive-rule';
|
|
case 'iface':
|
|
case 'iface_in':
|
|
case 'iface_out':
|
|
return 'fwlive-iface';
|
|
case 'dir':
|
|
return 'fwlive-dir';
|
|
case 'proto':
|
|
return 'fwlive-proto';
|
|
case 'src':
|
|
case 'dst':
|
|
return 'fwlive-addr';
|
|
case 'sport':
|
|
case 'dport':
|
|
return 'fwlive-port';
|
|
case 'flags':
|
|
return 'fwlive-flags';
|
|
case 'len':
|
|
return 'fwlive-len';
|
|
case 'flow':
|
|
return 'fwlive-flow-cell';
|
|
case 'message':
|
|
return 'fwlive-message fwlive-th-message';
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function flowCell(row, state, callbacks) {
|
|
const parts = [];
|
|
const onFilterClick = callbacks.onFilterClick;
|
|
const pushAddr = (addr, port, addrField, portField) => {
|
|
if (!addr && !port) return;
|
|
|
|
if (addr)
|
|
parts.push(
|
|
links.addrFilterLink(
|
|
addrField,
|
|
addr,
|
|
!!state.showHostnames,
|
|
state.hostnameCache,
|
|
onFilterClick
|
|
)
|
|
);
|
|
if (port) {
|
|
if (addr) parts.push(':');
|
|
parts.push(links.filterLink(portField, port, port, onFilterClick));
|
|
}
|
|
};
|
|
|
|
pushAddr(row.src, row.sport, 'src', 'sport');
|
|
if (parts.length && (row.dst || row.dport))
|
|
parts.push(E('span', { 'class': 'fwlive-flow-arrow' }, [' → ']));
|
|
pushAddr(row.dst, row.dport, 'dst', 'dport');
|
|
|
|
if (!parts.length) return '—';
|
|
|
|
return E('span', { 'class': 'fwlive-flow' }, parts);
|
|
}
|
|
|
|
function buildColumnCell(col, row, state, callbacks) {
|
|
const onFilterClick = callbacks.onFilterClick;
|
|
const msgDisplay = log.formatMessageDisplay(row.message, state.messageLayout);
|
|
const actionCell =
|
|
row.action && row.action !== 'unknown'
|
|
? links.filterLink(
|
|
'action',
|
|
row.action,
|
|
log.formatActionLabel(row.action),
|
|
onFilterClick
|
|
)
|
|
: log.formatActionLabel(row.action);
|
|
|
|
switch (col) {
|
|
case 'time': {
|
|
const timeAttrs = { 'class': columnCellClass(col) };
|
|
if (state.viewMode === 'simple')
|
|
timeAttrs.title = _('Click a row for the full message');
|
|
return E('td', timeAttrs, [
|
|
state.viewMode === 'simple'
|
|
? log.formatTimestampCompact(row.timestamp)
|
|
: log.formatTimestampLocal(row.timestamp)
|
|
]);
|
|
}
|
|
case 'action':
|
|
return E('td', { 'class': log.actionRowClass(row.action) }, [actionCell]);
|
|
case 'rule':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
links.ruleAdminLink(
|
|
row.rule_hint,
|
|
row.rule_label,
|
|
state.firewallBackend,
|
|
onFilterClick
|
|
)
|
|
]);
|
|
case 'iface':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
links.ifaceLink(row.interface_in, onFilterClick)
|
|
]);
|
|
case 'iface_in':
|
|
case 'iface_out':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
links.ifaceLink(
|
|
col === 'iface_in' ? row.interface_in : row.interface_out,
|
|
onFilterClick
|
|
)
|
|
]);
|
|
case 'dir':
|
|
return E('td', { 'class': columnCellClass(col) }, [log.formatCell(row.direction)]);
|
|
case 'proto':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
links.filterLink('proto', row.proto, null, onFilterClick)
|
|
]);
|
|
case 'src':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
links.addrFilterLink(
|
|
'src',
|
|
row.src,
|
|
!!state.showHostnames,
|
|
state.hostnameCache,
|
|
onFilterClick
|
|
)
|
|
]);
|
|
case 'sport':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
links.filterLink('sport', row.sport, null, onFilterClick)
|
|
]);
|
|
case 'dst':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
links.addrFilterLink(
|
|
'dst',
|
|
row.dst,
|
|
!!state.showHostnames,
|
|
state.hostnameCache,
|
|
onFilterClick
|
|
)
|
|
]);
|
|
case 'dport':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
links.filterLink('dport', row.dport, null, onFilterClick)
|
|
]);
|
|
case 'flags':
|
|
return E('td', { 'class': columnCellClass(col) }, [log.formatCell(row.flags)]);
|
|
case 'len':
|
|
return E('td', { 'class': columnCellClass(col) }, [
|
|
row.length != null ? String(row.length) : ''
|
|
]);
|
|
case 'flow':
|
|
return E('td', { 'class': columnCellClass(col) }, [flowCell(row, state, callbacks)]);
|
|
case 'message':
|
|
if (state.messageLayout === 'wrap') {
|
|
return E(
|
|
'td',
|
|
{
|
|
'class': 'fwlive-message',
|
|
'title': msgDisplay || ''
|
|
},
|
|
E('div', { 'class': 'fwlive-message-wrap' }, [msgDisplay || '—'])
|
|
);
|
|
}
|
|
return E(
|
|
'td',
|
|
{
|
|
'class': 'fwlive-message',
|
|
'title': msgDisplay || ''
|
|
},
|
|
[msgDisplay || '—']
|
|
);
|
|
default:
|
|
return E('td', {}, ['']);
|
|
}
|
|
}
|
|
|
|
function renderThead(host, state, _callbacks) {
|
|
const columns = state.columns || [];
|
|
const tr = host.querySelector('thead tr');
|
|
if (!tr) return;
|
|
|
|
let colgroup = host.querySelector('colgroup');
|
|
|
|
if (!colgroup) {
|
|
colgroup = E('colgroup', {});
|
|
host.insertBefore(colgroup, host.firstChild);
|
|
}
|
|
|
|
colgroup.innerHTML = '';
|
|
tr.innerHTML = '';
|
|
|
|
for (let i = 0; i < columns.length; i++) {
|
|
const col = columns[i];
|
|
colgroup.appendChild(
|
|
E('col', { 'class': 'fwlive-col fwlive-col-' + col.replace(/_/g, '-') })
|
|
);
|
|
tr.appendChild(E('th', { 'class': columnCellClass(col) }, [columnLabel(col)]));
|
|
}
|
|
}
|
|
|
|
function renderRows(host, state, callbacks) {
|
|
const rows = state.rows || [];
|
|
const columns = state.columns || [];
|
|
|
|
host.innerHTML = '';
|
|
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const r = rows[i];
|
|
const rowClass = [
|
|
i % 2 ? 'fwlive-row-alt' : '',
|
|
state.viewMode === 'simple' ? 'fwlive-row-clickable' : '',
|
|
state.expandedRowId === r.id ? 'fwlive-row-expanded' : '',
|
|
state.rowTint ? callbacks.actionRowTintClass(r.action) : ''
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
const cells = [];
|
|
for (let c = 0; c < columns.length; c++)
|
|
cells.push(buildColumnCell(columns[c], r, state, callbacks));
|
|
|
|
const tr = E(
|
|
'tr',
|
|
{
|
|
'class': rowClass,
|
|
'click': state.viewMode === 'simple' ? (ev) => callbacks.onRowClick(r.id, ev) : null
|
|
},
|
|
cells
|
|
);
|
|
host.appendChild(tr);
|
|
|
|
if (state.viewMode === 'simple' && state.expandedRowId === r.id) {
|
|
host.appendChild(
|
|
E('tr', { 'class': 'fwlive-msg-expand' }, [
|
|
E('td', { 'colspan': String(columns.length) }, [
|
|
E('div', { 'class': 'fwlive-msg-expand-label' }, [_('Message')]),
|
|
E('pre', { 'class': 'fwlive-msg-expand-body' }, [
|
|
log.formatMessageDisplay(r.message, 'wrap') || '—'
|
|
])
|
|
])
|
|
])
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return baseclass.extend({
|
|
renderThead: renderThead,
|
|
renderRows: renderRows
|
|
});
|