]> git.proxmox.com Git - pve-manager.git/commitdiff
ui: improve boot order editor
authorStefan Reiter <s.reiter@proxmox.com>
Tue, 6 Oct 2020 13:32:18 +0000 (15:32 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 14 Oct 2020 10:31:14 +0000 (12:31 +0200)
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>
www/css/ext6-pve.css
www/manager6/qemu/BootOrderEdit.js
www/manager6/qemu/Options.js

index a91f1aaf3d7a2257e985d26db9f3c39db9e2fdcf..6430ffc4a51dc06b8e376c008171e549fd775e90 100644 (file)
@@ -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;
index 19d5d50ab2232014a5dfe8839e11ee4a5626790f..c5726e96e89c234dc89ab52fbb8d6cfcc9572c44 100644 (file)
+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);
     },
 
+    isBootdev: function(dev, value) {
+       return this.isDisk(dev) ||
+           (/^net\d+/).test(dev) ||
+           (/^hostpci\d+/).test(dev) ||
+           ((/^usb\d+/).test(dev) && !(/spice/).test(value));
+    },
+
     setVMConfig: function(vmconfig) {
-       var me = this;
+       let 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.store.removeAll();
 
-       me.list.push(['d', 'CD-ROM']);
-       me.list.push(['n', gettext('Network')]);
-       me.list.push(['__none__', Proxmox.Utils.noneText]);
+       let boot = PVE.Parser.parsePropertyString(me.vmconfig.boot, "legacy");
 
-       me.recomputeList();
+       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;
 
-       me.comboboxes.forEach(function(box) {
-           box.resetOriginalValue();
-       });
-    },
+           // get the first 4 characters (acdn)
+           // ignore the rest (there should never be more than 4)
+           let orderList = order.split('').slice(0,4);
 
-    onGetValues: function(values) {
-       var me = this;
-       var order = me.selection.join('');
-       var res = { boot: order };
+           // 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);
+                       }
+                   });
+               }
 
-       if  (me.bootdisk && order.indexOf('c') !== -1) {
-           res.bootdisk = me.bootdisk;
-       } else {
-           res['delete'] = 'bootdisk';
+               // Object.each iterates in random order, sort alphabetically
+               list.sort();
+               list.forEach(dev => bootorder.push({name: dev, enabled: true}));
+           }
        }
 
-       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;
+       // 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}));
 
-           // 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);
-           }
+       // 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('proxmoxKVComboBox', {
-               fieldLabel: gettext('Boot device') + " 1",
-               labelWidth: 120,
-               name: 'bd1',
-               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',
+           columns: [
+               {
+                   header: '',
+                   renderer: () => "<i class='fa fa-reorder cursor-move'></i>",
+                   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 "<span class='faded'>" + idx + "</span>";
+                       }
+                   },
+               },
+               {
+                   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) {
index 20f6ffbb42d32662ea41efe59d8ee349e27010ef..1f07d81ad34bdae3681284837333f5f1294d867a 100644 (file)
@@ -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;
                        }