The API only sets 'active' for active interfaces, so it is either 1 or missing. As 1 compares neither greater nor less than undefined, sorting by the 'Active' column kept the previous order. The same applies to the 'Autostart' and 'VLAN aware' columns. Declare those fields as booleans, so that missing values get converted to false and the interfaces can be sorted by them. The shown values do not change, as missing values were already rendered as 'No'. Signed-off-by: Michal Fox <me@dualfroz.com>
461 lines
17 KiB
JavaScript
461 lines
17 KiB
JavaScript
Ext.define('proxmox-networks', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
{ name: 'active', type: 'boolean' },
|
|
'address',
|
|
'address6',
|
|
'altnames',
|
|
{ name: 'autostart', type: 'boolean' },
|
|
'bridge_ports',
|
|
{ name: 'bridge_vlan_aware', type: 'boolean' },
|
|
'cidr',
|
|
'cidr6',
|
|
'comments',
|
|
'gateway',
|
|
'gateway6',
|
|
'iface',
|
|
'netmask',
|
|
'netmask6',
|
|
'slaves',
|
|
'type',
|
|
'vlan-id',
|
|
'vlan-raw-device',
|
|
],
|
|
idProperty: 'iface',
|
|
});
|
|
|
|
Ext.define('Proxmox.node.NetworkView', {
|
|
extend: 'Ext.panel.Panel',
|
|
|
|
alias: ['widget.proxmoxNodeNetworkView'],
|
|
|
|
// defines what types of network devices we want to create
|
|
// order is always the same
|
|
types: ['bridge', 'bond', 'vlan', 'ovs'],
|
|
|
|
showApplyBtn: false,
|
|
|
|
// if true, the altnames column will be shown by default
|
|
showAltNames: false,
|
|
|
|
// for options passed down to the network edit window
|
|
editOptions: {},
|
|
|
|
initComponent: function () {
|
|
let me = this;
|
|
|
|
if (!me.nodename) {
|
|
throw 'no node name specified';
|
|
}
|
|
|
|
let baseUrl = `/nodes/${me.nodename}/network`;
|
|
|
|
// sort numbers in names by value, so that 'eno2' comes before 'eno10'
|
|
let naturalCompare = (a, b) =>
|
|
(a ?? '').localeCompare(b ?? '', undefined, { numeric: true });
|
|
|
|
let getPortsOrSlaves = (data) => {
|
|
if (data.type === 'bridge') {
|
|
return data.bridge_ports;
|
|
} else if (data.type === 'bond') {
|
|
return data.slaves;
|
|
} else if (data.type === 'OVSBridge') {
|
|
return data.ovs_ports;
|
|
} else if (data.type === 'OVSBond') {
|
|
return data.ovs_bonds;
|
|
}
|
|
return '';
|
|
};
|
|
|
|
// shared with the name column, so that its header shows the initial sort order
|
|
let ifaceSorter = Ext.create('Ext.util.Sorter', {
|
|
id: 'iface',
|
|
sorterFn: (a, b) => naturalCompare(a.data.iface, b.data.iface),
|
|
});
|
|
|
|
let store = Ext.create('Ext.data.Store', {
|
|
model: 'proxmox-networks',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json' + baseUrl,
|
|
},
|
|
sorters: [ifaceSorter],
|
|
});
|
|
|
|
let reload = function () {
|
|
let changeitem = me.down('#changes');
|
|
let apply_btn = me.down('#apply');
|
|
let revert_btn = me.down('#revert');
|
|
Proxmox.Utils.API2Request({
|
|
url: baseUrl,
|
|
failure: function (response, opts) {
|
|
store.loadData({});
|
|
Proxmox.Utils.setErrorMask(me, response.htmlStatus);
|
|
changeitem.update('');
|
|
changeitem.setHidden(true);
|
|
},
|
|
success: function (response, opts) {
|
|
let result = Ext.decode(response.responseText);
|
|
store.loadData(result.data);
|
|
let changes = result.changes;
|
|
if (changes === undefined || changes === '') {
|
|
changes = gettext('No changes');
|
|
changeitem.setHidden(true);
|
|
apply_btn.setDisabled(true);
|
|
revert_btn.setDisabled(true);
|
|
} else {
|
|
changeitem.update('<pre>' + Ext.htmlEncode(changes) + '</pre>');
|
|
changeitem.setHidden(false);
|
|
apply_btn.setDisabled(false);
|
|
revert_btn.setDisabled(false);
|
|
}
|
|
},
|
|
});
|
|
};
|
|
|
|
let run_editor = function () {
|
|
let grid = me.down('gridpanel');
|
|
let sm = grid.getSelectionModel();
|
|
let rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
Ext.create('Proxmox.node.NetworkEdit', {
|
|
autoShow: true,
|
|
nodename: me.nodename,
|
|
iface: rec.data.iface,
|
|
iftype: rec.data.type,
|
|
showAltNames: me.showAltNames,
|
|
...me.editOptions,
|
|
listeners: {
|
|
destroy: () => reload(),
|
|
},
|
|
});
|
|
};
|
|
|
|
let edit_btn = new Ext.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
handler: run_editor,
|
|
});
|
|
|
|
let sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
let del_btn = new Proxmox.button.StdRemoveButton({
|
|
selModel: sm,
|
|
getUrl: ({ data }) => `${baseUrl}/${data.iface}`,
|
|
callback: () => reload(),
|
|
});
|
|
|
|
let apply_btn = Ext.create('Proxmox.button.Button', {
|
|
text: gettext('Apply Configuration'),
|
|
itemId: 'apply',
|
|
disabled: true,
|
|
confirmMsg: 'Do you want to apply pending network changes?',
|
|
hidden: !me.showApplyBtn,
|
|
handler: function () {
|
|
Proxmox.Utils.API2Request({
|
|
url: baseUrl,
|
|
method: 'PUT',
|
|
waitMsgTarget: me,
|
|
success: function ({ result }, opts) {
|
|
Ext.create('Proxmox.window.TaskProgress', {
|
|
autoShow: true,
|
|
taskDone: reload,
|
|
upid: result.data,
|
|
});
|
|
},
|
|
failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
|
});
|
|
},
|
|
});
|
|
|
|
let set_button_status = function () {
|
|
let rec = sm.getSelection()[0];
|
|
|
|
edit_btn.setDisabled(!rec);
|
|
del_btn.setDisabled(!rec);
|
|
};
|
|
|
|
let findNextFreeInterfaceId = function (prefix) {
|
|
for (let next = 0; next <= 9999; next++) {
|
|
let id = `${prefix}${next.toString()}`;
|
|
if (!store.getById(id)) {
|
|
return id;
|
|
}
|
|
}
|
|
Ext.Msg.alert('Error', `No free ID for ${prefix} found!`);
|
|
return '';
|
|
};
|
|
|
|
let menu_items = [];
|
|
let addEditWindowToMenu = (iType, iDefault) => {
|
|
menu_items.push({
|
|
text: Proxmox.Utils.render_network_iface_type(iType),
|
|
handler: () =>
|
|
Ext.create('Proxmox.node.NetworkEdit', {
|
|
autoShow: true,
|
|
nodename: me.nodename,
|
|
iftype: iType,
|
|
iface_default: findNextFreeInterfaceId(iDefault ?? iType),
|
|
...me.editOptions,
|
|
onlineHelp: 'sysadmin_network_configuration',
|
|
listeners: {
|
|
destroy: () => reload(),
|
|
},
|
|
}),
|
|
});
|
|
};
|
|
|
|
if (me.types.indexOf('bridge') !== -1) {
|
|
addEditWindowToMenu('bridge', 'vmbr');
|
|
}
|
|
|
|
if (me.types.indexOf('bond') !== -1) {
|
|
addEditWindowToMenu('bond');
|
|
}
|
|
|
|
if (me.types.indexOf('vlan') !== -1) {
|
|
addEditWindowToMenu('vlan');
|
|
}
|
|
|
|
if (me.types.indexOf('ovs') !== -1) {
|
|
if (menu_items.length > 0) {
|
|
menu_items.push({ xtype: 'menuseparator' });
|
|
}
|
|
|
|
addEditWindowToMenu('OVSBridge', 'vmbr');
|
|
addEditWindowToMenu('OVSBond', 'bond');
|
|
|
|
menu_items.push({
|
|
text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
|
|
handler: () =>
|
|
Ext.create('Proxmox.node.NetworkEdit', {
|
|
autoShow: true,
|
|
nodename: me.nodename,
|
|
iftype: 'OVSIntPort',
|
|
listeners: {
|
|
destroy: () => reload(),
|
|
},
|
|
}),
|
|
});
|
|
}
|
|
|
|
let renderer_generator = function (fieldname) {
|
|
return function (val, metaData, rec) {
|
|
let tmp = [];
|
|
if (rec.data[fieldname]) {
|
|
tmp.push(rec.data[fieldname]);
|
|
}
|
|
if (rec.data[fieldname + '6']) {
|
|
tmp.push(rec.data[fieldname + '6']);
|
|
}
|
|
return tmp.join('<br>') || '';
|
|
};
|
|
};
|
|
|
|
Ext.apply(me, {
|
|
layout: 'border',
|
|
tbar: [
|
|
{
|
|
text: gettext('Create'),
|
|
menu: {
|
|
plain: true,
|
|
items: menu_items,
|
|
},
|
|
},
|
|
'-',
|
|
{
|
|
text: gettext('Revert'),
|
|
itemId: 'revert',
|
|
handler: function () {
|
|
Proxmox.Utils.API2Request({
|
|
url: baseUrl,
|
|
method: 'DELETE',
|
|
waitMsgTarget: me,
|
|
callback: function () {
|
|
reload();
|
|
},
|
|
failure: (response) =>
|
|
Ext.Msg.alert(gettext('Error'), response.htmlStatus),
|
|
});
|
|
},
|
|
},
|
|
edit_btn,
|
|
del_btn,
|
|
'-',
|
|
apply_btn,
|
|
],
|
|
items: [
|
|
{
|
|
xtype: 'gridpanel',
|
|
stateful: true,
|
|
stateId: 'grid-node-network',
|
|
store: store,
|
|
selModel: sm,
|
|
region: 'center',
|
|
border: false,
|
|
columns: [
|
|
{
|
|
header: gettext('Name'),
|
|
sortable: true,
|
|
dataIndex: 'iface',
|
|
sorter: ifaceSorter,
|
|
},
|
|
{
|
|
header: gettext('Alternative Names'),
|
|
dataIndex: 'altnames',
|
|
hidden: !me.showAltNames,
|
|
width: 140, // enough space for 'enx<MAC>'
|
|
renderer: (value) => {
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
if (Ext.isArray(value)) {
|
|
return value.map(Ext.htmlEncode).join('<br>');
|
|
}
|
|
return Ext.htmlEncode(value);
|
|
},
|
|
},
|
|
{
|
|
header: gettext('Type'),
|
|
sortable: true,
|
|
width: 120,
|
|
renderer: Proxmox.Utils.render_network_iface_type,
|
|
dataIndex: 'type',
|
|
},
|
|
{
|
|
xtype: 'booleancolumn',
|
|
header: gettext('Active'),
|
|
width: 80,
|
|
sortable: true,
|
|
dataIndex: 'active',
|
|
trueText: Proxmox.Utils.yesText,
|
|
falseText: Proxmox.Utils.noText,
|
|
undefinedText: Proxmox.Utils.noText,
|
|
},
|
|
{
|
|
xtype: 'booleancolumn',
|
|
header: gettext('Autostart'),
|
|
width: 80,
|
|
sortable: true,
|
|
dataIndex: 'autostart',
|
|
trueText: Proxmox.Utils.yesText,
|
|
falseText: Proxmox.Utils.noText,
|
|
undefinedText: Proxmox.Utils.noText,
|
|
},
|
|
{
|
|
xtype: 'booleancolumn',
|
|
header: gettext('VLAN aware'),
|
|
width: 80,
|
|
sortable: true,
|
|
dataIndex: 'bridge_vlan_aware',
|
|
trueText: Proxmox.Utils.yesText,
|
|
falseText: Proxmox.Utils.noText,
|
|
undefinedText: Proxmox.Utils.noText,
|
|
},
|
|
{
|
|
header: gettext('Ports/Slaves'),
|
|
dataIndex: 'type',
|
|
renderer: (value, metaData, { data }) => getPortsOrSlaves(data),
|
|
sorter: (a, b) =>
|
|
naturalCompare(getPortsOrSlaves(a.data), getPortsOrSlaves(b.data)),
|
|
},
|
|
{
|
|
header: gettext('Bond Mode'),
|
|
dataIndex: 'bond_mode',
|
|
renderer: Proxmox.Utils.render_bond_mode,
|
|
},
|
|
{
|
|
header: gettext('Hash Policy'),
|
|
hidden: true,
|
|
dataIndex: 'bond_xmit_hash_policy',
|
|
},
|
|
{
|
|
header: gettext('IP address'),
|
|
sortable: true,
|
|
width: 120,
|
|
hidden: true,
|
|
dataIndex: 'address',
|
|
renderer: renderer_generator('address'),
|
|
},
|
|
{
|
|
header: gettext('Subnet mask'),
|
|
width: 120,
|
|
sortable: true,
|
|
hidden: true,
|
|
dataIndex: 'netmask',
|
|
renderer: renderer_generator('netmask'),
|
|
},
|
|
{
|
|
header: gettext('CIDR'),
|
|
width: 150,
|
|
sortable: true,
|
|
dataIndex: 'cidr',
|
|
renderer: renderer_generator('cidr'),
|
|
},
|
|
{
|
|
header: gettext('Gateway'),
|
|
width: 150,
|
|
sortable: true,
|
|
dataIndex: 'gateway',
|
|
renderer: renderer_generator('gateway'),
|
|
},
|
|
{
|
|
header: gettext('VLAN ID'),
|
|
hidden: true,
|
|
sortable: true,
|
|
dataIndex: 'vlan-id',
|
|
},
|
|
{
|
|
header: gettext('VLAN raw device'),
|
|
hidden: true,
|
|
sortable: true,
|
|
dataIndex: 'vlan-raw-device',
|
|
},
|
|
{
|
|
header: 'MTU',
|
|
hidden: true,
|
|
sortable: true,
|
|
dataIndex: 'mtu',
|
|
},
|
|
{
|
|
header: gettext('Comment'),
|
|
dataIndex: 'comments',
|
|
flex: 1,
|
|
renderer: Ext.String.htmlEncode,
|
|
},
|
|
],
|
|
listeners: {
|
|
selectionchange: set_button_status,
|
|
itemdblclick: run_editor,
|
|
},
|
|
},
|
|
{
|
|
border: false,
|
|
region: 'south',
|
|
autoScroll: true,
|
|
hidden: true,
|
|
itemId: 'changes',
|
|
tbar: [
|
|
gettext('Pending changes') +
|
|
' (' +
|
|
gettext(
|
|
"Either reboot or use 'Apply Configuration' (needs ifupdown2) to activate",
|
|
) +
|
|
')',
|
|
],
|
|
split: true,
|
|
bodyPadding: 5,
|
|
flex: 0.6,
|
|
html: gettext('No changes'),
|
|
},
|
|
],
|
|
});
|
|
|
|
me.callParent();
|
|
reload();
|
|
},
|
|
});
|