* [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 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