public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [RFC widget-toolkit 0/2] fix #4442: Firewall log filtering
@ 2023-01-30  9:07 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
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Christian Ebner @ 2023-01-30  9:07 UTC (permalink / raw)
  To: pve-devel

This patch series introduces a firewall log view panel which allows to filter
firewall logs (including rotated logs) by date and time, up to minute precision.

The existing date time field was extended to publish state changes for value
bindings to be updated.

Suggestions for improvements, especially regarding the still incorrect update of
min/max values of the date and time fields on changes of the other one are
highly appreciated.

proxmox-widget-toolkit:

Christian Ebner (2):
  DateTimeField: Extend and refactor to make field value bindable
  fix #4442: panel: Add firewall log view panel

 src/Makefile                 |   1 +
 src/form/DateTimeField.js    | 104 ++++++++---
 src/panel/FirewallLogView.js | 350 +++++++++++++++++++++++++++++++++++
 3 files changed, 432 insertions(+), 23 deletions(-)
 create mode 100644 src/panel/FirewallLogView.js

pve-manager:

Christian Ebner (1):
  fix #4442: node/qemu: Use firewallLogView panel for firewall logs

 www/manager6/node/Config.js | 5 +++--
 www/manager6/qemu/Config.js | 5 +++--
 2 files changed, 6 insertions(+), 4 deletions(-)

-- 
2.30.2





^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pve-devel] [RFC widget-toolkit 1/2] DateTimeField: Extend and refactor to make field value bindable
  2023-01-30  9:07 [pve-devel] [RFC widget-toolkit 0/2] fix #4442: Firewall log filtering Christian Ebner
@ 2023-01-30  9:07 ` 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 ` [pve-devel] [RFC widget-toolkit 2/2] fix #4442: panel: Add firewall log view panel Christian Ebner
  2 siblings, 0 replies; 4+ messages in thread
From: Christian Ebner @ 2023-01-30  9:07 UTC (permalink / raw)
  To: pve-devel

Extends the date time field so that bindings are updated on value changes.
Also adds a config to disable child components and avoid modification of
current values by cloning the referenced object for min/max value calculation.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
 src/form/DateTimeField.js | 104 +++++++++++++++++++++++++++++---------
 1 file changed, 81 insertions(+), 23 deletions(-)

diff --git a/src/form/DateTimeField.js b/src/form/DateTimeField.js
index a061e15..fbf6b09 100644
--- a/src/form/DateTimeField.js
+++ b/src/form/DateTimeField.js
@@ -8,19 +8,33 @@ Ext.define('Proxmox.DateTimeField', {
 
     submitFormat: 'U',
 
-    getValue: function() {
+    config: {
+	disabled: false,
+    },
+
+    setValue: function(value) {
 	let me = this;
-	let d = me.lookupReference('dateentry').getValue();
+	me.setDate(value);
+	me.setTime(value);
 
-	if (d === undefined || d === null) { return null; }
+	// Notify all 'value' bindings of state change
+	me.publishState('value', value);
+    },
+
+    getValue: function() {
+	let me = this;
+	let date = me.lookupReference('dateentry').getValue();
 
-	let t = me.lookupReference('timeentry').getValue();
+	if (date === undefined || date === null) { return null; }
 
-	if (t === undefined || t === null) { return null; }
+	let time = me.lookupReference('timeentry').getValue();
 
-	let offset = (t.getHours() * 3600 + t.getMinutes() * 60) * 1000;
+	if (time === undefined || time === null) { return null; }
 
-	return new Date(d.getTime() + offset);
+	date.setHours(time.getHours());
+	date.setMinutes(time.getMinutes());
+	date.setSeconds(time.getSeconds());
+	return date;
     },
 
     getSubmitValue: function() {
@@ -31,6 +45,20 @@ Ext.define('Proxmox.DateTimeField', {
         return value ? Ext.Date.format(value, format) : null;
     },
 
+    setDate: function(date) {
+	let me = this;
+	let dateEntry = me.lookupReference('dateentry');
+	dateEntry.setValue(date);
+	dateEntry.publishState('value', date);
+    },
+
+    setTime: function(time) {
+	let me = this;
+	let timeEntry = me.lookupReference('timeentry');
+	timeEntry.setValue(time);
+	timeEntry.publishState('value', time);
+    },
+
     items: [
 	{
 	    xtype: 'datefield',
@@ -38,6 +66,17 @@ Ext.define('Proxmox.DateTimeField', {
 	    reference: 'dateentry',
 	    flex: 1,
 	    format: 'Y-m-d',
+	    bind: {
+		disabled: '{disabled}',
+	    },
+	    listeners: {
+		'change': function(field, newValue, oldValue) {
+		    let dateTimeField = field.up('fieldcontainer');
+		    dateTimeField.setDate(newValue);
+		    let value = dateTimeField.getValue();
+		    dateTimeField.publishState('value', value);
+		},
+	    },
 	},
 	{
 	    xtype: 'timefield',
@@ -46,6 +85,17 @@ Ext.define('Proxmox.DateTimeField', {
 	    width: 80,
 	    value: '00:00',
 	    increment: 60,
+	    bind: {
+		disabled: '{disabled}',
+	    },
+	    listeners: {
+		'change': function(field, newValue, oldValue) {
+		    let dateTimeField = field.up('fieldcontainer');
+		    dateTimeField.setTime(newValue);
+		    let value = dateTimeField.getValue();
+		    dateTimeField.publishState('value', value);
+		},
+	    },
 	},
     ],
 
@@ -56,21 +106,23 @@ Ext.define('Proxmox.DateTimeField', {
 	    return;
 	}
 
-	let minhours = value.getHours();
-	let minminutes = value.getMinutes();
+	// Clone to avoid modifying the referenced value
+	let clone = new Date(value);
+	let minhours = clone.getHours();
+	let minminutes = clone.getMinutes();
 
 	let hours = current.getHours();
 	let minutes = current.getMinutes();
 
-	value.setHours(0);
-	value.setMinutes(0);
-	value.setSeconds(0);
+	clone.setHours(0);
+	clone.setMinutes(0);
+	clone.setSeconds(0);
 	current.setHours(0);
 	current.setMinutes(0);
 	current.setSeconds(0);
 
 	let time = new Date();
-	if (current-value > 0) {
+	if (current-clone > 0) {
 	    time.setHours(0);
 	    time.setMinutes(0);
 	    time.setSeconds(0);
@@ -84,9 +136,9 @@ Ext.define('Proxmox.DateTimeField', {
 	// current time is smaller than the time part of the new minimum
 	// so we have to add 1 to the day
 	if (minhours*60+minminutes > hours*60+minutes) {
-	    value.setDate(value.getDate()+1);
+	    clone.setDate(clone.getDate()+1);
 	}
-	me.lookup('dateentry').setMinValue(value);
+	me.lookup('dateentry').setMinValue(clone);
     },
 
     setMaxValue: function(value) {
@@ -96,19 +148,25 @@ Ext.define('Proxmox.DateTimeField', {
 	    return;
 	}
 
-	let maxhours = value.getHours();
-	let maxminutes = value.getMinutes();
+	// Clone to avoid modifying the referenced value
+	let clone = new Date(value);
+	let maxhours = clone.getHours();
+	let maxminutes = clone.getMinutes();
 
 	let hours = current.getHours();
 	let minutes = current.getMinutes();
 
-	value.setHours(0);
-	value.setMinutes(0);
+	clone.setHours(0);
+	clone.setMinutes(0);
+	clone.setSeconds(0);
+	clone.setMilliseconds(0);
 	current.setHours(0);
 	current.setMinutes(0);
+	current.setSeconds(0);
+	current.setMilliseconds(0);
 
 	let time = new Date();
-	if (value-current > 0) {
+	if (clone-current > 0) {
 	    time.setHours(23);
 	    time.setMinutes(59);
 	    time.setSeconds(59);
@@ -118,13 +176,13 @@ Ext.define('Proxmox.DateTimeField', {
 	}
 	me.lookup('timeentry').setMaxValue(time);
 
-	// current time is biger than the time part of the new maximum
+	// current time is bigger than the time part of the new maximum
 	// so we have to subtract 1 to the day
 	if (maxhours*60+maxminutes < hours*60+minutes) {
-	    value.setDate(value.getDate()-1);
+	    clone.setDate(clone.getDate()-1);
 	}
 
-	me.lookup('dateentry').setMaxValue(value);
+	me.lookup('dateentry').setMaxValue(clone);
     },
 
     initComponent: function() {
-- 
2.30.2





^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pve-devel] [RFC manager 1/1] fix #4442: node/qemu: Use firewallLogView panel for firewall logs
  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 ` Christian Ebner
  2023-01-30  9:07 ` [pve-devel] [RFC widget-toolkit 2/2] fix #4442: panel: Add firewall log view panel Christian Ebner
  2 siblings, 0 replies; 4+ messages in thread
From: Christian Ebner @ 2023-01-30  9:07 UTC (permalink / raw)
  To: pve-devel

Using the firewall log view panel allows to filter not only by date, but also by
time (up to minute precision).

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
---
 www/manager6/node/Config.js | 5 +++--
 www/manager6/qemu/Config.js | 5 +++--
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/www/manager6/node/Config.js b/www/manager6/node/Config.js
index f8577b0f..31a1fc7f 100644
--- a/www/manager6/node/Config.js
+++ b/www/manager6/node/Config.js
@@ -387,12 +387,13 @@ Ext.define('PVE.node.Config', {
 	if (caps.nodes['Sys.Syslog']) {
 	    me.items.push(
 		{
-		    xtype: 'proxmoxLogView',
+		    xtype: 'proxmoxFirewallLogView',
 		    title: gettext('Log'),
 		    iconCls: 'fa fa-list',
 		    groups: ['firewall'],
 		    onlineHelp: 'chapter_pve_firewall',
-		    url: '/api2/extjs/nodes/' + nodename + '/firewall/log',
+		    url: '/nodes/' + nodename + '/firewall/log',
+		    log_select_timespan: true,
 		    itemId: 'firewall-fwlog',
 		},
 		{
diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js
index 94c540c5..f78ebc67 100644
--- a/www/manager6/qemu/Config.js
+++ b/www/manager6/qemu/Config.js
@@ -383,8 +383,9 @@ Ext.define('PVE.qemu.Config', {
 		    iconCls: 'fa fa-list',
 		    onlineHelp: 'chapter_pve_firewall',
 		    itemId: 'firewall-fwlog',
-		    xtype: 'proxmoxLogView',
-		    url: '/api2/extjs' + base_url + '/firewall/log',
+		    xtype: 'proxmoxFirewallLogView',
+		    url: base_url + '/firewall/log',
+		    log_select_timespan: true,
 		},
 	    );
 	}
-- 
2.30.2





^ permalink raw reply	[flat|nested] 4+ messages in thread

* [pve-devel] [RFC widget-toolkit 2/2] fix #4442: panel: Add firewall log view panel
  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
  2 siblings, 0 replies; 4+ messages in thread
From: Christian Ebner @ 2023-01-30  9:07 UTC (permalink / raw)
  To: pve-devel

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





^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2023-01-30  9:08 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [pve-devel] [RFC widget-toolkit 2/2] fix #4442: panel: Add firewall log view panel Christian Ebner

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal