mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-07-09 22:13:49 +00:00
ui: improve boot order editor
The new boot order property can express many more scenarios than the old one. Update the editor so it can handle it. Features a grid with all supported boot devices which can be reordered using drag-and-drop, as well as toggled on and off with an inline checkbox. Support for configs still using the old format is given, with the first write automatically updating the VM config to use the new one. The renderer for the Options panel is updated with support for the new format. Note that it is very well possible to disable all boot devices, in which case an empty 'boot: ' will be stored to the config file. I'm not sure what that would be useful for, but there's no reason to forbid it either, just warn the user that it's probably not what they want. Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
This commit is contained in:
parent
659a335026
commit
1cc7c672b7
@ -583,6 +583,10 @@ table.osds td:first-of-type {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cursor-move {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.x-grid-filters-filtered-column {
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
|
@ -1,150 +1,250 @@
|
||||
Ext.define('pve-boot-order-entry', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
{name: 'name', type: 'string'},
|
||||
{name: 'enabled', type: 'bool'},
|
||||
{name: 'desc', type: 'string'},
|
||||
]
|
||||
});
|
||||
|
||||
Ext.define('PVE.qemu.BootOrderPanel', {
|
||||
extend: 'Proxmox.panel.InputPanel',
|
||||
alias: 'widget.pveQemuBootOrderPanel',
|
||||
|
||||
vmconfig: {}, // store loaded vm config
|
||||
store: undefined,
|
||||
|
||||
bootdisk: undefined,
|
||||
selection: [],
|
||||
list: [],
|
||||
comboboxes: [],
|
||||
inUpdate: false,
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
},
|
||||
|
||||
isBootDisk: function(value) {
|
||||
isDisk: function(value) {
|
||||
return PVE.Utils.bus_match.test(value);
|
||||
},
|
||||
|
||||
setVMConfig: function(vmconfig) {
|
||||
var me = this;
|
||||
me.vmconfig = vmconfig;
|
||||
var order = me.vmconfig.boot || 'cdn';
|
||||
me.bootdisk = me.vmconfig.bootdisk || undefined;
|
||||
|
||||
// get the first 3 characters
|
||||
// ignore the rest (there should never be more than 3)
|
||||
me.selection = order.split('').slice(0,3);
|
||||
|
||||
// build bootdev list
|
||||
me.list = [];
|
||||
Ext.Object.each(me.vmconfig, function(key, value) {
|
||||
if (me.isBootDisk(key) &&
|
||||
!(/media=cdrom/).test(value)) {
|
||||
me.list.push([key, "Disk '" + key + "'"]);
|
||||
}
|
||||
});
|
||||
|
||||
me.list.push(['d', 'CD-ROM']);
|
||||
me.list.push(['n', gettext('Network')]);
|
||||
me.list.push(['__none__', Proxmox.Utils.noneText]);
|
||||
|
||||
me.recomputeList();
|
||||
|
||||
me.comboboxes.forEach(function(box) {
|
||||
box.resetOriginalValue();
|
||||
});
|
||||
isBootdev: function(dev, value) {
|
||||
return this.isDisk(dev) ||
|
||||
(/^net\d+/).test(dev) ||
|
||||
(/^hostpci\d+/).test(dev) ||
|
||||
((/^usb\d+/).test(dev) && !(/spice/).test(value));
|
||||
},
|
||||
|
||||
onGetValues: function(values) {
|
||||
var me = this;
|
||||
var order = me.selection.join('');
|
||||
var res = { boot: order };
|
||||
setVMConfig: function(vmconfig) {
|
||||
let me = this;
|
||||
me.vmconfig = vmconfig;
|
||||
|
||||
if (me.bootdisk && order.indexOf('c') !== -1) {
|
||||
res.bootdisk = me.bootdisk;
|
||||
} else {
|
||||
res['delete'] = 'bootdisk';
|
||||
me.store.removeAll();
|
||||
|
||||
let boot = PVE.Parser.parsePropertyString(me.vmconfig.boot, "legacy");
|
||||
|
||||
let bootorder = [];
|
||||
if (boot.order) {
|
||||
bootorder = boot.order.split(';').map(dev => ({name: dev, enabled: true}));
|
||||
} else if (!(/^\s*$/).test(me.vmconfig.boot)) {
|
||||
// legacy style, transform to new bootorder
|
||||
let order = boot.legacy || 'cdn';
|
||||
let bootdisk = me.vmconfig.bootdisk || undefined;
|
||||
|
||||
// get the first 4 characters (acdn)
|
||||
// ignore the rest (there should never be more than 4)
|
||||
let orderList = order.split('').slice(0,4);
|
||||
|
||||
// build bootdev list
|
||||
for (let i = 0; i < orderList.length; i++) {
|
||||
let list = [];
|
||||
if (orderList[i] === 'c') {
|
||||
if (bootdisk !== undefined && me.vmconfig[bootdisk]) {
|
||||
list.push(bootdisk);
|
||||
}
|
||||
} else if (orderList[i] === 'd') {
|
||||
Ext.Object.each(me.vmconfig, function(key, value) {
|
||||
if (me.isDisk(key) && (/media=cdrom/).test(value)) {
|
||||
list.push(key);
|
||||
}
|
||||
});
|
||||
} else if (orderList[i] === 'n') {
|
||||
Ext.Object.each(me.vmconfig, function(key, value) {
|
||||
if ((/^net\d+/).test(key)) {
|
||||
list.push(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Object.each iterates in random order, sort alphabetically
|
||||
list.sort();
|
||||
list.forEach(dev => bootorder.push({name: dev, enabled: true}));
|
||||
}
|
||||
}
|
||||
|
||||
// add disabled devices as well
|
||||
let disabled = [];
|
||||
Ext.Object.each(me.vmconfig, function(key, value) {
|
||||
if (me.isBootdev(key, value) &&
|
||||
!Ext.Array.some(bootorder, x => x.name === key))
|
||||
{
|
||||
disabled.push(key);
|
||||
}
|
||||
});
|
||||
disabled.sort();
|
||||
disabled.forEach(dev => bootorder.push({name: dev, enabled: false}));
|
||||
|
||||
// add descriptions
|
||||
bootorder.forEach(entry => {
|
||||
entry.desc = me.vmconfig[entry.name];
|
||||
});
|
||||
|
||||
me.store.insert(0, bootorder);
|
||||
me.store.fireEvent("update");
|
||||
},
|
||||
|
||||
calculateValue: function() {
|
||||
let me = this;
|
||||
return me.store.getData().items
|
||||
.filter(x => x.data.enabled)
|
||||
.map(x => x.data.name)
|
||||
.join(';');
|
||||
},
|
||||
|
||||
onGetValues: function() {
|
||||
let me = this;
|
||||
// Note: we allow an empty value, so no 'delete' option
|
||||
let val = { order: me.calculateValue() };
|
||||
let res = { boot: PVE.Parser.printPropertyString(val) };
|
||||
return res;
|
||||
},
|
||||
|
||||
recomputeSelection: function(combobox, newVal, oldVal) {
|
||||
var me = this.up('#inputpanel');
|
||||
me.selection = [];
|
||||
me.comboboxes.forEach(function(item) {
|
||||
var val = item.getValue();
|
||||
|
||||
// when selecting an already selected item,
|
||||
// switch it around
|
||||
if ((val === newVal || (me.isBootDisk(val) && me.isBootDisk(newVal))) &&
|
||||
item.name !== combobox.name &&
|
||||
newVal !== '__none__') {
|
||||
// swap items
|
||||
val = oldVal;
|
||||
}
|
||||
|
||||
// push 'c','d' or 'n' in the array
|
||||
if (me.isBootDisk(val)) {
|
||||
me.selection.push('c');
|
||||
me.bootdisk = val;
|
||||
} else if (val === 'd' ||
|
||||
val === 'n') {
|
||||
me.selection.push(val);
|
||||
}
|
||||
});
|
||||
|
||||
me.recomputeList();
|
||||
items: [
|
||||
{
|
||||
xtype: 'grid',
|
||||
reference: 'grid',
|
||||
margin: '0 0 5 0',
|
||||
columns: [
|
||||
{
|
||||
header: '',
|
||||
renderer: () => "<i class='fa fa-reorder cursor-move'></i>",
|
||||
width: 30,
|
||||
sortable: false,
|
||||
hideable: false,
|
||||
draggable: false,
|
||||
},
|
||||
|
||||
recomputeList: function(){
|
||||
var me = this;
|
||||
// set the correct values in the kvcomboboxes
|
||||
var cnt = 0;
|
||||
me.comboboxes.forEach(function(item) {
|
||||
if (cnt === 0) {
|
||||
// never show 'none' on first combobox
|
||||
item.store.loadData(me.list.slice(0, me.list.length-1));
|
||||
{
|
||||
header: '#',
|
||||
width: 30,
|
||||
sortable: false,
|
||||
hideable: false,
|
||||
draggable: false,
|
||||
renderer: (value, metaData, record, rowIndex) => {
|
||||
let idx = (rowIndex + 1).toString();
|
||||
if (record.get('enabled')) {
|
||||
return idx;
|
||||
} else {
|
||||
item.store.loadData(me.list);
|
||||
return "<span class='faded'>" + idx + "</span>";
|
||||
}
|
||||
item.suspendEvent('change');
|
||||
if (cnt < me.selection.length) {
|
||||
item.setValue((me.selection[cnt] !== 'c')?me.selection[cnt]:me.bootdisk);
|
||||
} else if (cnt === 0){
|
||||
item.setValue('');
|
||||
} else {
|
||||
item.setValue('__none__');
|
||||
}
|
||||
cnt++;
|
||||
item.resumeEvent('change');
|
||||
item.validate();
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'checkcolumn',
|
||||
header: gettext('Enabled'),
|
||||
dataIndex: 'enabled',
|
||||
width: 70,
|
||||
sortable: false,
|
||||
hideable: false,
|
||||
draggable: false,
|
||||
},
|
||||
{
|
||||
header: gettext('Device'),
|
||||
dataIndex: 'name',
|
||||
width: 70,
|
||||
sortable: false,
|
||||
hideable: false,
|
||||
draggable: false,
|
||||
},
|
||||
{
|
||||
header: gettext('Description'),
|
||||
dataIndex: 'desc',
|
||||
flex: true,
|
||||
sortable: false,
|
||||
hideable: false,
|
||||
draggable: false,
|
||||
},
|
||||
],
|
||||
viewConfig: {
|
||||
plugins: {
|
||||
ptype: 'gridviewdragdrop',
|
||||
dragText: gettext('Drag and drop to reorder'),
|
||||
}
|
||||
},
|
||||
listeners: {
|
||||
drop: function() {
|
||||
// doesn't fire automatically on reorder
|
||||
this.getStore().fireEvent("update");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'component',
|
||||
html: gettext('Drag and drop to reorder'),
|
||||
},
|
||||
{
|
||||
xtype: 'displayfield',
|
||||
reference: 'emptyWarning',
|
||||
userCls: 'pmx-hint',
|
||||
value: gettext('Warning: No devices selected, the VM will probably not boot!'),
|
||||
},
|
||||
{
|
||||
// for dirty marking and 'reset' function
|
||||
xtype: 'field',
|
||||
reference: 'marker',
|
||||
hidden: true,
|
||||
setValue: function(val) {
|
||||
let me = this;
|
||||
let panel = me.up('pveQemuBootOrderPanel');
|
||||
|
||||
// on form reset, go back to original state
|
||||
if (!panel.inUpdate) {
|
||||
panel.setVMConfig(panel.vmconfig);
|
||||
}
|
||||
|
||||
// not a subclass, so no callParent; just do it manually
|
||||
me.setRawValue(me.valueToRaw(val));
|
||||
return me.mixins.field.setValue.call(me, val);
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
|
||||
// this has to be done here, because of
|
||||
// the way our inputPanel class handles items
|
||||
me.comboboxes = [
|
||||
Ext.createWidget('proxmoxKVComboBox', {
|
||||
fieldLabel: gettext('Boot device') + " 1",
|
||||
labelWidth: 120,
|
||||
name: 'bd1',
|
||||
allowBlank: false,
|
||||
listeners: {
|
||||
change: me.recomputeSelection
|
||||
}
|
||||
}),
|
||||
Ext.createWidget('proxmoxKVComboBox', {
|
||||
fieldLabel: gettext('Boot device') + " 2",
|
||||
labelWidth: 120,
|
||||
name: 'bd2',
|
||||
allowBlank: false,
|
||||
listeners: {
|
||||
change: me.recomputeSelection
|
||||
}
|
||||
}),
|
||||
Ext.createWidget('proxmoxKVComboBox', {
|
||||
fieldLabel: gettext('Boot device') + " 3",
|
||||
labelWidth: 120,
|
||||
name: 'bd3',
|
||||
allowBlank: false,
|
||||
listeners: {
|
||||
change: me.recomputeSelection
|
||||
}
|
||||
})
|
||||
];
|
||||
Ext.apply(me, { items: me.comboboxes });
|
||||
me.callParent();
|
||||
|
||||
let controller = me.getController();
|
||||
|
||||
let grid = controller.lookup('grid');
|
||||
let marker = controller.lookup('marker');
|
||||
let emptyWarning = controller.lookup('emptyWarning');
|
||||
|
||||
marker.originalValue = undefined;
|
||||
|
||||
me.store = Ext.create('Ext.data.Store', {
|
||||
model: 'pve-boot-order-entry',
|
||||
listeners: {
|
||||
update: function() {
|
||||
this.commitChanges();
|
||||
let val = me.calculateValue();
|
||||
if (marker.originalValue === undefined) {
|
||||
marker.originalValue = val;
|
||||
}
|
||||
me.inUpdate = true;
|
||||
marker.setValue(val);
|
||||
me.inUpdate = false;
|
||||
marker.checkDirty();
|
||||
emptyWarning.setHidden(val !== '');
|
||||
grid.getView().refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
grid.setStore(me.store);
|
||||
}
|
||||
});
|
||||
|
||||
@ -157,9 +257,10 @@ Ext.define('PVE.qemu.BootOrderEdit', {
|
||||
}],
|
||||
|
||||
subject: gettext('Boot Order'),
|
||||
width: 600,
|
||||
|
||||
initComponent : function() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
me.callParent();
|
||||
me.load({
|
||||
success: function(response, options) {
|
||||
|
@ -92,27 +92,45 @@ Ext.define('PVE.qemu.Options', {
|
||||
editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.BootOrderEdit' : undefined,
|
||||
multiKey: ['boot', 'bootdisk'],
|
||||
renderer: function(order, metaData, record, rowIndex, colIndex, store, pending) {
|
||||
if (/^\s*$/.test(order)) {
|
||||
return gettext('(No boot device selected)');
|
||||
}
|
||||
let boot = PVE.Parser.parsePropertyString(order, "legacy");
|
||||
if (boot.order) {
|
||||
let list = boot.order.split(';');
|
||||
let ret = '';
|
||||
let i = 1;
|
||||
list.forEach(dev => {
|
||||
if (ret) {
|
||||
ret += ', ';
|
||||
}
|
||||
ret += dev;
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
// legacy style and fallback
|
||||
var i;
|
||||
var text = '';
|
||||
var bootdisk = me.getObjectValue('bootdisk', undefined, pending);
|
||||
order = order || 'cdn';
|
||||
order = boot.legacy || 'cdn';
|
||||
for (i = 0; i < order.length; i++) {
|
||||
var sel = order.substring(i, i + 1);
|
||||
if (text) {
|
||||
text += ', ';
|
||||
}
|
||||
var sel = order.substring(i, i + 1);
|
||||
if (sel === 'c') {
|
||||
if (bootdisk) {
|
||||
text += "Disk '" + bootdisk + "'";
|
||||
text += bootdisk;
|
||||
} else {
|
||||
text += "Disk";
|
||||
text += gettext('(no bootdisk)');
|
||||
}
|
||||
} else if (sel === 'n') {
|
||||
text += 'Network';
|
||||
text += gettext('any net');
|
||||
} else if (sel === 'a') {
|
||||
text += 'Floppy';
|
||||
text += gettext('Floppy');
|
||||
} else if (sel === 'd') {
|
||||
text += 'CD-ROM';
|
||||
text += gettext('any CD-ROM');
|
||||
} else {
|
||||
text += sel;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user