diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css index a91f1aaf..6430ffc4 100644 --- a/www/css/ext6-pve.css +++ b/www/css/ext6-pve.css @@ -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; diff --git a/www/manager6/qemu/BootOrderEdit.js b/www/manager6/qemu/BootOrderEdit.js index 19d5d50a..c5726e96 100644 --- a/www/manager6/qemu/BootOrderEdit.js +++ b/www/manager6/qemu/BootOrderEdit.js @@ -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(); - }, - - 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(); - }); - }, - - initComponent : function() { - var 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 + items: [ + { + xtype: 'grid', + reference: 'grid', + margin: '0 0 5 0', + columns: [ + { + header: '', + renderer: () => "", + width: 30, + sortable: false, + hideable: false, + draggable: false, + }, + { + 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 { + return "" + idx + ""; + } + }, + }, + { + 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'), } - }), - Ext.createWidget('proxmoxKVComboBox', { - fieldLabel: gettext('Boot device') + " 2", - labelWidth: 120, - name: 'bd2', - allowBlank: false, - listeners: { - change: me.recomputeSelection + }, + listeners: { + drop: function() { + // doesn't fire automatically on reorder + this.getStore().fireEvent("update"); } - }), - Ext.createWidget('proxmoxKVComboBox', { - fieldLabel: gettext('Boot device') + " 3", - labelWidth: 120, - name: 'bd3', - allowBlank: false, - listeners: { - change: me.recomputeSelection + }, + }, + { + 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 }); + + // 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() { + let me = this; + 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) { diff --git a/www/manager6/qemu/Options.js b/www/manager6/qemu/Options.js index 20f6ffbb..1f07d81a 100644 --- a/www/manager6/qemu/Options.js +++ b/www/manager6/qemu/Options.js @@ -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; }