all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Christian Ebner <c.ebner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [RFC widget-toolkit 2/2] fix #4442: panel: Add firewall log view panel
Date: Mon, 30 Jan 2023 10:07:15 +0100	[thread overview]
Message-ID: <20230130090715.349057-4-c.ebner@proxmox.com> (raw)
In-Reply-To: <20230130090715.349057-1-c.ebner@proxmox.com>

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@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





      parent reply	other threads:[~2023-01-30  9:08 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-01-30  9:07 [pve-devel] [RFC widget-toolkit 0/2] fix #4442: Firewall log filtering Christian Ebner
2023-01-30  9:07 ` [pve-devel] [RFC widget-toolkit 1/2] DateTimeField: Extend and refactor to make field value bindable Christian Ebner
2023-01-30  9:07 ` [pve-devel] [RFC manager 1/1] fix #4442: node/qemu: Use firewallLogView panel for firewall logs Christian Ebner
2023-01-30  9:07 ` Christian Ebner [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20230130090715.349057-4-c.ebner@proxmox.com \
    --to=c.ebner@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal