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 D829780BF7 for ; Fri, 19 Nov 2021 15:43:08 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id C60FD2C883 for ; Fri, 19 Nov 2021 15:42:38 +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 id 409102C84C for ; Fri, 19 Nov 2021 15:42:36 +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 0E5B743F0A for ; Fri, 19 Nov 2021 15:42:30 +0100 (CET) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Fri, 19 Nov 2021 15:42:27 +0100 Message-Id: <20211119144227.1337999-4-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20211119144227.1337999-1-d.csapak@proxmox.com> References: <20211119144227.1337999-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.210 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [data.network, values.network, rec.data] Subject: [pbs-devel] [PATCH proxmox-backup 2/2] ui: add Traffic Control UI X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 19 Nov 2021 14:43:08 -0000 adds a list of traffic control rules (with their current usage) and let the user add/edit/remove them the edit window currently has a grid for timeframes to add/remove with input fields for start/endtime and checkboxes for the days there are still some improvements possible, like having a seperate grid for networks (the input field is maybe too small), or optimizing consecutive days to a range (e.g. mon..wed instead of mon,tue,wed) Signed-off-by: Dominik Csapak --- www/Makefile | 2 + www/NavigationTree.js | 6 + www/config/TrafficControlView.js | 197 +++++++++++++ www/window/TrafficControlEdit.js | 464 +++++++++++++++++++++++++++++++ 4 files changed, 669 insertions(+) create mode 100644 www/config/TrafficControlView.js create mode 100644 www/window/TrafficControlEdit.js diff --git a/www/Makefile b/www/Makefile index 32a6d7d5..616c3e12 100644 --- a/www/Makefile +++ b/www/Makefile @@ -47,6 +47,7 @@ JSSRC= \ config/UserView.js \ config/TokenView.js \ config/RemoteView.js \ + config/TrafficControlView.js \ config/ACLView.js \ config/SyncView.js \ config/VerifyView.js \ @@ -60,6 +61,7 @@ JSSRC= \ window/DataStoreEdit.js \ window/NotesEdit.js \ window/RemoteEdit.js \ + window/TrafficControlEdit.js \ window/NotifyOptions.js \ window/SyncJobEdit.js \ window/UserEdit.js \ diff --git a/www/NavigationTree.js b/www/NavigationTree.js index 6035526c..3b4e54ce 100644 --- a/www/NavigationTree.js +++ b/www/NavigationTree.js @@ -50,6 +50,12 @@ Ext.define('PBS.store.NavigationStore', { path: 'pbsRemoteView', leaf: true, }, + { + text: gettext('Traffic Control'), + iconCls: 'fa fa-exchange fa-rotate-90', + path: 'pbsTrafficControlView', + leaf: true, + }, { text: gettext('Certificates'), iconCls: 'fa fa-certificate', diff --git a/www/config/TrafficControlView.js b/www/config/TrafficControlView.js new file mode 100644 index 00000000..70532d6c --- /dev/null +++ b/www/config/TrafficControlView.js @@ -0,0 +1,197 @@ +Ext.define('pmx-traffic-control', { + extend: 'Ext.data.Model', + fields: [ + 'name', 'rate-in', 'rate-out', 'burst-in', 'burst-out', 'network', + 'timeframe', 'comment', 'cur-rate-in', 'cur-rate-out', + { + name: 'rateInUsed', + calculate: function(data) { + return (data['cur-rate-in'] || 0) / (data['rate-in'] || Infinity); + }, + }, + { + name: 'rateOutUsed', + calculate: function(data) { + return (data['cur-rate-out'] || 0) / (data['rate-out'] || Infinity); + }, + }, + ], + idProperty: 'name', + proxy: { + type: 'proxmox', + url: '/api2/json/admin/traffic-control', + }, +}); + +Ext.define('PBS.config.TrafficControlView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pbsTrafficControlView', + + stateful: true, + stateId: 'grid-traffic-control', + + title: gettext('Traffic Control'), + +// tools: [PBS.Utils.get_help_tool("backup-remote")], + + controller: { + xclass: 'Ext.app.ViewController', + + addRemote: function() { + let me = this; + Ext.create('PBS.window.TrafficControlEdit', { + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + editRemote: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection.length < 1) return; + + Ext.create('PBS.window.TrafficControlEdit', { + name: selection[0].data.name, + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + render_bandwidth: (value) => value ? Proxmox.Utils.format_size(value) + '/s' : '', + + reload: function() { this.getView().getStore().rstore.load(); }, + + init: function(view) { + Proxmox.Utils.monStoreErrors(view, view.getStore().rstore); + }, + }, + + listeners: { + activate: 'reload', + itemdblclick: 'editRemote', + }, + + store: { + type: 'diff', + autoDestroy: true, + autoDestroyRstore: true, + sorters: 'name', + rstore: { + type: 'update', + storeid: 'pmx-traffic-control', + model: 'pmx-traffic-control', + autoStart: true, + interval: 5000, + }, + }, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Add'), + handler: 'addRemote', + selModel: false, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + handler: 'editRemote', + disabled: true, + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: '/config/traffic-control', + callback: 'reload', + }, + ], + + viewConfig: { + trackOver: false, + }, + + columns: [ + { + header: gettext('Rule'), + width: 200, + sortable: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'name', + }, + { + header: gettext('Rate In'), + width: 200, + sortable: true, + renderer: 'render_bandwidth', + dataIndex: 'rate-in', + }, + { + header: gettext('Rate In Used'), + xtype: 'widgetcolumn', + dataIndex: 'rateInUsed', + widget: { + xtype: 'progressbarwidget', + textTpl: '{percent:number("0")}%', + animate: true, + }, + }, + { + header: gettext('Rate Out'), + width: 200, + sortable: true, + renderer: 'render_bandwidth', + dataIndex: 'rate-out', + }, + { + header: gettext('Rate Out Used'), + xtype: 'widgetcolumn', + dataIndex: 'rateOutUsed', + widget: { + xtype: 'progressbarwidget', + textTpl: '{percent:number("0")}%', + animate: true, + }, + }, + { + header: gettext('Burst In'), + width: 200, + sortable: true, + renderer: 'render_bandwidth', + dataIndex: 'burst-in', + }, + { + header: gettext('Burst Out'), + width: 200, + sortable: true, + renderer: 'render_bandwidth', + dataIndex: 'burst-out', + }, + { + header: gettext('Networks'), + width: 200, + sortable: true, + renderer: Ext.String.htmlEncode, + dataIndex: 'network', + }, + { + header: gettext('Timeframes'), + sortable: false, + renderer: (timeframes) => Ext.String.htmlEncode(timeframes.join('; ')), + dataIndex: 'timeframe', + width: 200, + }, + { + header: gettext('Comment'), + sortable: false, + renderer: Ext.String.htmlEncode, + dataIndex: 'comment', + flex: 1, + }, + ], +}); diff --git a/www/window/TrafficControlEdit.js b/www/window/TrafficControlEdit.js new file mode 100644 index 00000000..24e6b63f --- /dev/null +++ b/www/window/TrafficControlEdit.js @@ -0,0 +1,464 @@ +Ext.define('PBS.window.TrafficControlEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pbsTrafficControlEdit', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'sysadmin_traffic_control', + width: 800, + + isAdd: true, + + subject: gettext('Traffic Control Rule'), + + fieldDefaults: { labelWidth: 120 }, + + cbindData: function(initialConfig) { + let me = this; + + let baseurl = '/api2/extjs/config/traffic-control'; + let name = initialConfig.name; + + me.isCreate = !name; + me.url = name ? `${baseurl}/${name}` : baseurl; + me.method = name ? 'PUT' : 'POST'; + return { }; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + weekdays: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + + dowChanged: function(field, value) { + let me = this; + let record = field.getWidgetRecord(); + if (record === undefined) { + // this is sometimes called before a record/column is initialized + return; + } + let col = field.getWidgetColumn(); + record.set(col.dataIndex, value); + record.commit(); + + me.updateTimeframeField(); + }, + + timeChanged: function(field, value) { + let me = this; + if (value === null) { + return; + } + let record = field.getWidgetRecord(); + if (record === undefined) { + // this is sometimes called before a record/column is initialized + return; + } + let col = field.getWidgetColumn(); + let hours = value.getHours().toString().padStart(2, '0'); + let minutes = value.getMinutes().toString().padStart(2, '0'); + record.set(col.dataIndex, `${hours}:${minutes}`); + record.commit(); + + me.updateTimeframeField(); + }, + + addTimeframe: function() { + let me = this; + me.lookup('timeframes').getStore().add({ + start: "00:00", + end: "23:59", + mon: true, + tue: true, + wed: true, + thu: true, + fri: true, + sat: true, + sun: true, + }); + + me.updateTimeframeField(); + }, + + updateTimeframeField: function() { + let me = this; + + let timeframes = []; + me.lookup('timeframes').getStore().each((rec) => { + let timeframe = ''; + let days = me.weekdays.filter(day => rec.data[day]); + if (days.length < 7 && days.length > 0) { + timeframe += days.join(',') + ' '; + } + let { start, end } = rec.data; + + timeframe += `${start}-${end}`; + timeframes.push(timeframe); + }); + + let field = me.lookup('timeframe'); + field.suspendEvent('change'); + field.setValue(timeframes.join(';')); + field.resumeEvent('change'); + }, + + removeTimeFrame: function(field) { + let me = this; + let record = field.getWidgetRecord(); + if (record === undefined) { + // this is sometimes called before a record/column is initialized + return; + } + + me.lookup('timeframes').getStore().remove(record); + me.updateTimeframeField(); + }, + + parseTimeframe: function(timeframe) { + let me = this; + let [, days, start, end] = /^(?:(\S*)\s+)?([0-9:]+)-([0-9:]+)$/.exec(timeframe) || []; + + if (start === '0') { + start = "00:00"; + } + + let record = { + start, + end, + }; + + if (!days) { + days = 'mon..sun'; + } + + days = days.split(','); + days.forEach((day) => { + if (record[day]) { + return; + } + + if (me.weekdays.indexOf(day) !== -1) { + record[day] = true; + } else { + // we have a range 'xxx..yyy' + let [startDay, endDay] = day.split('..'); + let startIdx = me.weekdays.indexOf(startDay); + let endIdx = me.weekdays.indexOf(endDay); + + if (endIdx < startIdx) { + endIdx += me.weekdays.length; + } + + for (let dayIdx = startIdx; dayIdx <= endIdx; dayIdx++) { + let curDay = me.weekdays[dayIdx%me.weekdays.length]; + if (!record[curDay]) { + record[curDay] = true; + } + } + } + }); + + return record; + }, + + setGridData: function(field, value) { + let me = this; + if (!value) { + return; + } + + value = value.split(';'); + let records = value.map((timeframe) => me.parseTimeframe(timeframe)); + me.lookup('timeframes').getStore().setData(records); + }, + + control: { + 'grid checkbox': { + change: 'dowChanged', + }, + 'grid timefield': { + change: 'timeChanged', + }, + 'grid button': { + click: 'removeTimeFrame', + }, + 'field[name=timeframe]': { + change: 'setGridData', + }, + }, + }, + + items: { + xtype: 'inputpanel', + onGetValues: function(values) { + let me = this; + let isCreate = me.up('window').isCreate; + + if (values['network-select'] === 'all') { + values.network = '0.0.0.0/0'; + } else if (values.network) { + values.network = values.network.split(/\s*,\s*/); + } + + if (!Ext.isArray(values.timeframe)) { + values.timeframe = values.timeframe.split(';'); + } + + delete values['network-select']; + + if (!isCreate) { + PBS.Utils.delete_if_default(values, 'rate-in'); + PBS.Utils.delete_if_default(values, 'rate-out'); + PBS.Utils.delete_if_default(values, 'burst-in'); + PBS.Utils.delete_if_default(values, 'burst-out'); + if (typeof values.delete === 'string') { + values.delete = values.delete.split(','); + } + } + + return values; + }, + column1: [ + { + xtype: 'pmxDisplayEditField', + name: 'name', + fieldLabel: gettext('Name'), + renderer: Ext.htmlEncode, + allowBlank: false, + minLength: 4, + cbind: { + editable: '{isCreate}', + }, + }, + { + xtype: 'pmxBandwidthField', + fieldLabel: gettext('Rate In'), + name: 'rate-in', + }, + { + xtype: 'pmxBandwidthField', + fieldLabel: gettext('Rate Out'), + name: 'rate-out', + }, + ], + + column2: [ + { + xtype: 'proxmoxtextfield', + name: 'comment', + cbind: { + deleteEmpty: '{!isCreate}', + }, + fieldLabel: gettext('Comment'), + }, + { + xtype: 'pmxBandwidthField', + fieldLabel: gettext('Burst In'), + name: 'burst-in', + }, + { + xtype: 'pmxBandwidthField', + fieldLabel: gettext('Burst Out'), + name: 'burst-out', + }, + ], + + columnB: [ + { + xtype: 'fieldcontainer', + fieldLabel: gettext('Network'), + layout: { + type: 'hbox', + align: 'stretch', + }, + items: [ + { + flex: 1, + xtype: 'radiofield', + boxLabel: gettext('All Networks'), + name: 'network-select', + value: true, + inputValue: 'all', + }, + { + xtype: 'radiofield', + boxLabel: gettext('Limit to'), + name: 'network-select', + inputValue: 'limit', + listeners: { + change: function(field, value) { + this.up('window').lookup('network').setDisabled(!value); + }, + }, + }, + { + flex: 1, + margin: '0 0 0 10', + xtype: 'proxmoxtextfield', + name: 'network', + reference: 'network', + disabled: true, + }, + ], + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Timeframes'), + }, + { + xtype: 'fieldcontainer', + items: [ + { + xtype: 'grid', + height: 150, + scrollable: true, + reference: 'timeframes', + store: { + fields: ['start', 'end', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + data: [], + }, + columns: [ + { + text: gettext('Time Start'), + xtype: 'widgetcolumn', + dataIndex: 'start', + widget: { + xtype: 'timefield', + isFormField: false, + format: 'H:i', + formatText: 'HH:MM', + }, + flex: 1, + }, + { + text: gettext('Time End'), + xtype: 'widgetcolumn', + dataIndex: 'end', + widget: { + xtype: 'timefield', + isFormField: false, + format: 'H:i', + formatText: 'HH:MM', + maxValue: '23:59', + }, + flex: 1, + }, + { + text: gettext('Mon'), + xtype: 'widgetcolumn', + dataIndex: 'mon', + width: 60, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Tue'), + xtype: 'widgetcolumn', + dataIndex: 'tue', + width: 60, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Wed'), + xtype: 'widgetcolumn', + dataIndex: 'wed', + width: 60, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Thu'), + xtype: 'widgetcolumn', + dataIndex: 'thu', + width: 60, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Fri'), + xtype: 'widgetcolumn', + dataIndex: 'fri', + width: 60, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Sat'), + xtype: 'widgetcolumn', + dataIndex: 'sat', + width: 60, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Sun'), + xtype: 'widgetcolumn', + dataIndex: 'sun', + width: 60, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + xtype: 'widgetcolumn', + width: 40, + widget: { + xtype: 'button', + iconCls: 'fa fa-trash-o', + }, + }, + ], + }, + ], + }, + { + xtype: 'button', + text: gettext('Add'), + iconCls: 'fa fa-plus-circle', + handler: 'addTimeframe', + }, + { + xtype: 'hidden', + reference: 'timeframe', + name: 'timeframe', + }, + ], + }, + + initComponent: function() { + let me = this; + me.callParent(); + if (!me.isCreate) { + me.load({ + success: function(response) { + let data = response.result.data; + if (data.network?.length === 1 && data.network[0] === '0.0.0.0/0') { + data['network-select'] = 'all'; + delete data.network; + } else { + data['network-select'] = 'limit'; + } + + if (Ext.isArray(data.timeframe)) { + data.timeframe = data.timeframe.join(';'); + } + + me.setValues(data); + }, + }); + } + }, +}); -- 2.30.2