[pve-devel] [RFC widget-toolkit 2/2] fix #4442: panel: Add firewall log view panel

Christian Ebner c.ebner at proxmox.com
Mon Jan 30 10:07:15 CET 2023


Adds a custom firewall log view panel, based on the existing log view panel.
The firewall log view panel is extended to include `since` and `until` filters
with date and time filtering, in contrast to the date only filtering for the log
view panel.

Signed-off-by: Christian Ebner <c.ebner at proxmox.com>
---
 src/Makefile                 |   1 +
 src/panel/FirewallLogView.js | 350 +++++++++++++++++++++++++++++++++++
 2 files changed, 351 insertions(+)
 create mode 100644 src/panel/FirewallLogView.js

diff --git a/src/Makefile b/src/Makefile
index 95da5aa..16cc8f1 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -55,6 +55,7 @@ JSSRC=					\
 	panel/InputPanel.js		\
 	panel/InfoWidget.js		\
 	panel/LogView.js		\
+	panel/FirewallLogView.js	\
 	panel/NodeInfoRepoStatus.js	\
 	panel/JournalView.js		\
 	panel/PermissionView.js		\
diff --git a/src/panel/FirewallLogView.js b/src/panel/FirewallLogView.js
new file mode 100644
index 0000000..6528f7c
--- /dev/null
+++ b/src/panel/FirewallLogView.js
@@ -0,0 +1,350 @@
+/*
+ * Display firewall log entries in a panel with scrollbar
+ * The log entries are automatically refreshed via a background task,
+ * with newest entries coming at the bottom
+ */
+Ext.define('Proxmox.panel.FirewallLogView', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'proxmoxFirewallLogView',
+
+    pageSize: 510,
+    viewBuffer: 50,
+    lineHeight: 16,
+
+    scrollToEnd: true,
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	updateParams: function() {
+	    let me = this;
+	    let viewModel = me.getViewModel();
+
+	    if (viewModel.get('hide_timespan') || viewModel.get('livemode')) {
+		return;
+	    }
+
+	    let since = viewModel.get('since');
+	    let until = viewModel.get('until');
+
+
+	    if (since > until) {
+		Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
+		return;
+	    }
+
+	    viewModel.set('params.since', Ext.Date.format(since, 'U'));
+	    viewModel.set('params.until', Ext.Date.format(until, 'U'));
+	    me.getView().loadTask.delay(200);
+	},
+
+	scrollPosBottom: function() {
+	    let view = this.getView();
+	    let pos = view.getScrollY();
+	    let maxPos = view.getScrollable().getMaxPosition().y;
+	    return maxPos - pos;
+	},
+
+	updateView: function(lines, first, total) {
+	    let me = this;
+	    let view = me.getView();
+	    let viewModel = me.getViewModel();
+	    let content = me.lookup('content');
+	    let data = viewModel.get('data');
+
+	    if (first === data.first && total === data.total && lines.length === data.lines) {
+		// before there is any real output, we get 'no output' as a single line, so always
+		// update if we only have one to be sure to catch the first real line of output
+		if (total !== 1) {
+		    return; // same content, skip setting and scrolling
+		}
+	    }
+	    viewModel.set('data', {
+		first: first,
+		total: total,
+		lines: lines.length,
+	    });
+
+	    let scrollPos = me.scrollPosBottom();
+	    let scrollToBottom = view.scrollToEnd && scrollPos <= 5;
+
+	    if (!scrollToBottom) {
+		// so that we have the 'correct' height for the text
+		lines.length = total;
+	    }
+
+	    content.update(lines.join('<br>'));
+
+	    if (scrollToBottom) {
+		let scroller = view.getScrollable();
+		scroller.suspendEvent('scroll');
+		view.scrollTo(0, Infinity);
+		me.updateStart(true);
+		scroller.resumeEvent('scroll');
+	    }
+	},
+
+	doLoad: function() {
+	    let me = this;
+	    if (me.running) {
+		me.requested = true;
+		return;
+	    }
+	    me.running = true;
+	    let view = me.getView();
+	    Proxmox.Utils.API2Request({
+		url: me.getView().url,
+		params: me.getViewModel().get('params'),
+		method: 'GET',
+		success: function(response) {
+		    if (me.isDestroyed) {
+			return;
+		    }
+		    Proxmox.Utils.setErrorMask(me, false);
+		    let total = response.result.total;
+		    let lines = [];
+		    let first = Infinity;
+
+		    Ext.Array.each(response.result.data, function(line) {
+			if (first > line.n) {
+			    first = line.n;
+			}
+			lines[line.n - 1] = Ext.htmlEncode(line.t);
+		    });
+
+		    me.updateView(lines, first - 1, total);
+		    me.running = false;
+		    if (me.requested) {
+			me.requested = false;
+			view.loadTask.delay(200);
+		    }
+		},
+		failure: function(response) {
+		    if (view.failCallback) {
+			view.failCallback(response);
+		    } else {
+			let msg = response.htmlStatus;
+			Proxmox.Utils.setErrorMask(me, msg);
+		    }
+		    me.running = false;
+		    if (me.requested) {
+			me.requested = false;
+			view.loadTask.delay(200);
+		    }
+		},
+	    });
+	},
+
+	updateStart: function(scrolledToBottom, targetLine) {
+	    let me = this;
+	    let view = me.getView(), viewModel = me.getViewModel();
+
+	    let limit = viewModel.get('params.limit');
+	    let total = viewModel.get('data.total');
+
+	    // heuristic: scroll up? -> load more in front; scroll down? -> load more at end
+	    let startRatio = view.lastTargetLine && view.lastTargetLine > targetLine ? 2/3 : 1/3;
+	    view.lastTargetLine = targetLine;
+
+	    let newStart = scrolledToBottom
+		? Math.trunc(total - limit, 10)
+		: Math.trunc(targetLine - (startRatio * limit) + 10);
+
+	    viewModel.set('params.start', Math.max(newStart, 0));
+
+	    view.loadTask.delay(200);
+	},
+
+	onScroll: function(x, y) {
+	    let me = this;
+	    let view = me.getView(), viewModel = me.getViewModel();
+
+	    let line = view.getScrollY() / view.lineHeight;
+	    let viewLines = view.getHeight() / view.lineHeight;
+
+	    let viewStart = Math.max(Math.trunc(line - 1 - view.viewBuffer), 0);
+	    let viewEnd = Math.trunc(line + viewLines + 1 + view.viewBuffer);
+
+	    let { start, limit } = viewModel.get('params');
+
+	    let margin = start < 20 ? 0 : 20;
+
+	    if (viewStart < start + margin || viewEnd > start + limit - margin) {
+		me.updateStart(false, line);
+	    }
+	},
+
+	onLiveMode: function() {
+	    let me = this;
+	    let viewModel = me.getViewModel();
+	    viewModel.set('livemode', true);
+	    viewModel.set('params', { start: 0, limit: 510 });
+
+	    let view = me.getView();
+	    delete view.content;
+	    view.scrollToEnd = true;
+	    me.updateView([], true, false);
+	},
+
+	onTimespan: function() {
+	    let me = this;
+	    me.getViewModel().set('livemode', false);
+	    me.updateView([], false);
+	    // Directly apply currently selected values without update
+	    // button click.
+	    me.updateParams();
+	},
+
+	init: function(view) {
+	    let me = this;
+
+	    if (!view.url) {
+		throw "no url specified";
+	    }
+
+	    let viewModel = me.getViewModel();
+	    viewModel.set('until', new Date());
+	    viewModel.set('since', new Date());
+	    viewModel.set('params.limit', view.pageSize);
+	    viewModel.set('hide_timespan', !view.log_select_timespan);
+	    me.lookup('content').setStyle('line-height', `${view.lineHeight}px`);
+
+	    view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
+
+	    me.updateParams();
+	    view.task = Ext.TaskManager.start({
+		run: () => {
+		    if (!view.isVisible() || !view.scrollToEnd) {
+			return;
+		    }
+		    if (me.scrollPosBottom() <= 5) {
+			view.loadTask.delay(200);
+		    }
+		},
+		interval: 1000,
+	    });
+	},
+    },
+
+    onDestroy: function() {
+	let me = this;
+	me.loadTask.cancel();
+	Ext.TaskManager.stop(me.task);
+    },
+
+    // for user to initiate a load from outside
+    requestUpdate: function() {
+	let me = this;
+	me.loadTask.delay(200);
+    },
+
+    viewModel: {
+	data: {
+	    since: null,
+	    until: null,
+	    livemode: true,
+	    hide_timespan: false,
+	    data: {
+		start: 0,
+		total: 0,
+		textlen: 0,
+	    },
+	    params: {
+		start: 0,
+		limit: 510,
+	    },
+	},
+    },
+
+    layout: 'auto',
+    bodyPadding: 5,
+    scrollable: {
+	x: 'auto',
+	y: 'auto',
+	listeners: {
+	    // we have to have this here, since we cannot listen to events of the scroller in
+	    // the viewcontroller (extjs bug?), nor does the panel have a 'scroll' event'
+	    scroll: {
+		fn: function(scroller, x, y) {
+		    let controller = this.component.getController();
+		    if (controller) { // on destroy, controller can be gone
+			controller.onScroll(x, y);
+		    }
+		},
+		buffer: 200,
+	    },
+	},
+    },
+
+    tbar: {
+	items: [
+	    '->',
+	    {
+		xtype: 'segmentedbutton',
+		items: [
+		    {
+			text: gettext('Live Mode'),
+			bind: {
+			    pressed: '{livemode}',
+			},
+			handler: 'onLiveMode',
+		    },
+		    {
+			text: gettext('Select Timespan'),
+			bind: {
+			    pressed: '{!livemode}',
+			},
+			handler: 'onTimespan',
+		    },
+		],
+	    },
+	    {
+		xtype: 'box',
+		autoEl: { cn: gettext('Since') + ':' },
+	    },
+	    {
+		xtype: 'promxoxDateTimeField',
+		name: 'since_date_time',
+		reference: 'since',
+		bind: {
+		    disabled: '{livemode}',
+		    value: '{since}',
+		    maxValue: '{until}',
+		},
+	    },
+	    {
+		xtype: 'box',
+		autoEl: { cn: gettext('Until') + ':' },
+	    },
+	    {
+		xtype: 'promxoxDateTimeField',
+		name: 'until_date_time',
+		reference: 'until',
+		bind: {
+		    disabled: '{livemode}',
+		    value: '{until}',
+		    minValue: '{since}',
+		},
+	    },
+	    {
+		xtype: 'button',
+		text: 'Update',
+		bind: {
+		    disabled: '{livemode}',
+		},
+		handler: 'updateParams',
+	    },
+	],
+    },
+
+    items: [
+	{
+	    xtype: 'box',
+	    reference: 'content',
+	    style: {
+		font: 'normal 11px tahoma, arial, verdana, sans-serif',
+		'white-space': 'pre',
+	    },
+	},
+    ],
+});
-- 
2.30.2






More information about the pve-devel mailing list