ui: add Traffic Control UI
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>
This commit is contained in:
parent
4fe77c36df
commit
ac4e399a10
@ -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 \
|
||||
|
@ -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',
|
||||
|
197
www/config/TrafficControlView.js
Normal file
197
www/config/TrafficControlView.js
Normal file
@ -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,
|
||||
},
|
||||
],
|
||||
});
|
464
www/window/TrafficControlEdit.js
Normal file
464
www/window/TrafficControlEdit.js
Normal file
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue
Block a user