From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 84A1090AB8 for ; Mon, 30 Jan 2023 10:08:15 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4F557CE20 for ; Mon, 30 Jan 2023 10:07:45 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Mon, 30 Jan 2023 10:07:43 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 7FD8745175 for ; Mon, 30 Jan 2023 10:07:43 +0100 (CET) From: Christian Ebner To: pve-devel@lists.proxmox.com Date: Mon, 30 Jan 2023 10:07:15 +0100 Message-Id: <20230130090715.349057-4-c.ebner@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230130090715.349057-1-c.ebner@proxmox.com> References: <20230130090715.349057-1-c.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.000 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [RFC widget-toolkit 2/2] fix #4442: panel: Add firewall log view panel X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Mon, 30 Jan 2023 09:08:15 -0000 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 --- 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('
')); + + 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