+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: 'PVE.panel.InputPanel',
+ extend: 'Proxmox.panel.InputPanel',
alias: 'widget.pveQemuBootOrderPanel',
- vmconfig: {}, // store loaded vm config
- bootdisk: undefined,
- selection: [],
- list: [],
- comboboxes: [],
+ onlineHelp: 'qm_bootorder',
- setVMConfig: function(vmconfig) {
- var me = this;
- me.vmconfig = vmconfig;
- var order = me.vmconfig.boot || 'cdn';
- me.bootdisk = me.vmconfig.bootdisk || undefined;
+ vmconfig: {}, // store loaded vm config
+ store: undefined,
- // get the first 3 characters
- // ignore the rest (there should never be more than 3)
- me.selection = order.split('').slice(0,3);
+ inUpdate: false,
+ controller: {
+ xclass: 'Ext.app.ViewController',
- // build bootdev list
- me.list = [];
- Ext.Object.each(me.vmconfig, function(key, value) {
- if ((/^(ide|sata|scsi|virtio)\d+$/).test(key) &&
- !(/media=cdrom/).test(value)) {
- me.list.push([key, "Disk '" + key + "'"]);
- }
- });
+ init: function(view) {
+ let me = this;
- me.list.push(['d', 'CD-ROM']);
- me.list.push(['n', gettext('Network')]);
- me.list.push(['__none__', PVE.Utils.noneText]);
+ let grid = me.lookup('grid');
+ let marker = me.lookup('marker');
+ let emptyWarning = me.lookup('emptyWarning');
- me.recomputeList();
+ marker.originalValue = undefined;
- me.comboboxes.forEach(function(box) {
- box.resetOriginalValue();
- });
+ view.store = Ext.create('Ext.data.Store', {
+ model: 'pve-boot-order-entry',
+ listeners: {
+ update: function() {
+ this.commitChanges();
+ let val = view.calculateValue();
+ if (marker.originalValue === undefined) {
+ marker.originalValue = val;
+ }
+ view.inUpdate = true;
+ marker.setValue(val);
+ view.inUpdate = false;
+ marker.checkDirty();
+ emptyWarning.setHidden(val !== '');
+ grid.getView().refresh();
+ },
+ },
+ });
+ grid.setStore(view.store);
+ },
},
- onGetValues: function(values) {
- var me = this;
- var order = me.selection.join('');
- var res = { boot: order };
+ isCloudinit: (v) => v.match(/media=cdrom/) && v.match(/[:/]vm-\d+-cloudinit/),
- if (me.bootdisk && order.indexOf('c') !== -1) {
- res['bootdisk'] = me.bootdisk;
- } else {
- res['delete'] = 'bootdisk';
- }
+ isDisk: function(value) {
+ return PVE.Utils.bus_match.test(value);
+ },
- return res;
+ isBootdev: function(dev, value) {
+ return (this.isDisk(dev) && !this.isCloudinit(value)) ||
+ (/^net\d+/).test(dev) ||
+ (/^hostpci\d+/).test(dev) ||
+ ((/^usb\d+/).test(dev) && !(/spice/).test(value));
},
- 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 &&
- item.name !== combobox.name &&
- newVal !== '__none__') {
- // swap items
- val = oldVal;
+ setVMConfig: function(vmconfig) {
+ let me = this;
+ me.vmconfig = vmconfig;
+
+ 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) && value.match(/media=cdrom/) && !me.isCloudinit(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 }));
}
+ }
- // push 'c','d' or 'n' in the array
- if ((/^(ide|sata|scsi|virtio)\d+$/).test(val)) {
- me.selection.push('c');
- me.bootdisk = val;
- } else if (val === 'd' ||
- val === 'n') {
- me.selection.push(val);
+ // 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.recomputeList();
+ me.store.insert(0, bootorder);
+ me.store.fireEvent("update");
},
- 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));
- } else {
- item.store.loadData(me.list);
- }
- 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();
- });
+ calculateValue: function() {
+ let me = this;
+ return me.store.getData().items
+ .filter(x => x.data.enabled)
+ .map(x => x.data.name)
+ .join(';');
},
- initComponent : function() {
- var me = this;
-
- // this has to be done here, because of
- // the way our inputPanel class handles items
- me.comboboxes = [
- Ext.createWidget('pveKVComboBox', {
- fieldLabel: gettext('Boot device') + " 1",
- labelWidth: 120,
- name: 'bd1',
- allowBlank: false,
- listeners: {
- change: me.recomputeSelection,
- }
- }),
- Ext.createWidget('pveKVComboBox', {
- fieldLabel: gettext('Boot device') + " 2",
- labelWidth: 120,
- name: 'bd2',
- allowBlank: false,
- listeners: {
- change: me.recomputeSelection,
- }
- }),
- Ext.createWidget('pveKVComboBox', {
- fieldLabel: gettext('Boot device') + " 3",
- labelWidth: 120,
- name: 'bd3',
- allowBlank: false,
- listeners: {
- change: me.recomputeSelection,
+ 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;
+ },
+
+ items: [
+ {
+ xtype: 'grid',
+ reference: 'grid',
+ margin: '0 0 5 0',
+ minHeight: 150,
+ defaults: {
+ sortable: false,
+ hideable: false,
+ draggable: false,
+ },
+ columns: [
+ {
+ header: '#',
+ flex: 4,
+ renderer: (value, metaData, record, rowIndex) => {
+ let dragHandle = "<i class='pve-grid-fa fa fa-fw fa-reorder cursor-move'></i>";
+ let idx = (rowIndex + 1).toString();
+ if (record.get('enabled')) {
+ return dragHandle + idx;
+ } else {
+ return dragHandle + "<span class='faded'>" + idx + "</span>";
+ }
+ },
+ },
+ {
+ xtype: 'checkcolumn',
+ header: gettext('Enabled'),
+ dataIndex: 'enabled',
+ flex: 4,
+ },
+ {
+ header: gettext('Device'),
+ dataIndex: 'name',
+ flex: 6,
+ renderer: (value, metaData, record, rowIndex) => {
+ let desc = record.get('desc');
+
+ let icon = '', iconCls;
+ if (value.match(/^net\d+$/)) {
+ iconCls = 'exchange';
+ } else if (desc.match(/media=cdrom/)) {
+ metaData.tdCls = 'pve-itype-icon-cdrom';
+ } else {
+ iconCls = 'hdd-o';
+ }
+ if (iconCls !== undefined) {
+ metaData.tdCls += 'pve-itype-fa';
+ icon = `<i class="pve-grid-fa fa fa-fw fa-${iconCls}"></i>`;
+ }
+
+ return icon + value;
+ },
+ },
+ {
+ header: gettext('Description'),
+ dataIndex: 'desc',
+ flex: 20,
+ },
+ ],
+ 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);
}
- }),
- ];
- Ext.apply(me, { items: me.comboboxes });
- me.callParent();
- }
+
+ // not a subclass, so no callParent; just do it manually
+ me.setRawValue(me.valueToRaw(val));
+ return me.mixins.field.setValue.call(me, val);
+ },
+ },
+ ],
});
Ext.define('PVE.qemu.BootOrderEdit', {
- extend: 'PVE.window.Edit',
+ extend: 'Proxmox.window.Edit',
- items: {
+ items: [{
xtype: 'pveQemuBootOrderPanel',
itemId: 'inputpanel',
- },
+ }],
subject: gettext('Boot Order'),
+ width: 640,
- initComponent : function() {
- var me = this;
+ initComponent: function() {
+ let me = this;
me.callParent();
me.load({
- success: function(response, options) {
- me.down('#inputpanel').setVMConfig(response.result.data);
- }
+ success: ({ result }) => me.down('#inputpanel').setVMConfig(result.data),
});
- }
+ },
});