[pbs-devel] [PATCH proxmox-backup 2/2] ui: add Traffic Control UI
Dominik Csapak
d.csapak at proxmox.com
Fri Nov 19 15:42:27 CET 2021
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 <d.csapak at proxmox.com>
---
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
More information about the pbs-devel
mailing list