]> git.proxmox.com Git - proxmox-backup.git/commitdiff
ui: add Traffic Control UI
authorDominik Csapak <d.csapak@proxmox.com>
Fri, 19 Nov 2021 14:42:27 +0000 (15:42 +0100)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Sat, 20 Nov 2021 18:40:59 +0000 (19:40 +0100)
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@proxmox.com>
www/Makefile
www/NavigationTree.js
www/config/TrafficControlView.js [new file with mode: 0644]
www/window/TrafficControlEdit.js [new file with mode: 0644]

index 32a6d7d5bf0273474a58cfc45951532246beef09..616c3e129b15fa4abcb2b09a5132a01f4488f4d0 100644 (file)
@@ -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                              \
index 6035526c34b32fffa0628b21e25f256c875b513a..3b4e54ce2395b08a7a6c2459c719b7d03e13ba5f 100644 (file)
@@ -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 (file)
index 0000000..70532d6
--- /dev/null
@@ -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 (file)
index 0000000..24e6b63
--- /dev/null
@@ -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);
+               },
+           });
+       }
+    },
+});