]> git.proxmox.com Git - pve-manager.git/blobdiff - www/manager6/qemu/HardwareView.js
update shipped appliance info index
[pve-manager.git] / www / manager6 / qemu / HardwareView.js
index 43337c365111e2ab1f0b7b113b4f89faacba0a65..672a7e1a74a59a491744ebc9289b66c20f517b48 100644 (file)
@@ -1,5 +1,5 @@
 Ext.define('PVE.qemu.HardwareView', {
-    extend: 'PVE.grid.PendingObjectGrid',
+    extend: 'Proxmox.grid.PendingObjectGrid',
     alias: ['widget.PVE.qemu.HardwareView'],
 
     onlineHelp: 'qm_virtual_machines_settings',
@@ -8,65 +8,103 @@ Ext.define('PVE.qemu.HardwareView', {
        var me = this;
        var rows = me.rows;
        var rowdef = rows[key] || {};
+       var iconCls = rowdef.iconCls;
+       var icon = '';
+       var txt = rowdef.header || key;
 
        metaData.tdAttr = "valign=middle";
 
+       if (rowdef.isOnStorageBus) {
+           var value = me.getObjectValue(key, '', false);
+           if (value === '') {
+               value = me.getObjectValue(key, '', true);
+           }
+           if (value.match(/vm-.*-cloudinit/)) {
+               iconCls = 'cloud';
+               txt = rowdef.cloudheader;
+           } else if (value.match(/media=cdrom/)) {
+               metaData.tdCls = 'pve-itype-icon-cdrom';
+               return rowdef.cdheader;
+           }
+       }
+
        if (rowdef.tdCls) {
            metaData.tdCls = rowdef.tdCls;
-           if (rowdef.tdCls == 'pve-itype-icon-storage') { 
-               var value = me.getObjectValue(key, '', true);
-               if (value.match(/media=cdrom/)) {
-                   metaData.tdCls = 'pve-itype-icon-cdrom';
-                   return rowdef.cdheader;
-               }
-           }
+       } else if (iconCls) {
+           icon = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
+           metaData.tdCls += " pve-itype-fa";
+       }
+
+       // only return icons in grid but not remove dialog
+       if (rowIndex !== undefined) {
+           return icon + txt;
+       } else {
+           return txt;
        }
-       return rowdef.header || key;
     },
 
-    initComponent : function() {
+    initComponent: function() {
        var me = this;
-       var i, confid;
 
-       var nodename = me.pveSelNode.data.node;
-       if (!nodename) { 
+       const { node: nodename, vmid } = me.pveSelNode.data;
+       if (!nodename) {
            throw "no node name specified";
-       }
-
-       var vmid = me.pveSelNode.data.vmid;
-       if (!vmid) {
+       } else if (!vmid) {
            throw "no VM ID specified";
        }
 
-       var caps = Ext.state.Manager.get('GuiCap');
+       const caps = Ext.state.Manager.get('GuiCap');
+       const diskCap = caps.vms['VM.Config.Disk'];
+       const cdromCap = caps.vms['VM.Config.CDROM'];
+
+       let isCloudInitKey = v => v && v.toString().match(/vm-.*-cloudinit/);
 
-       var rows = {
+       const nodeInfo = PVE.data.ResourceStore.getNodes().find(node => node.node === nodename);
+       let processorEditor = {
+           xtype: 'pveQemuProcessorEdit',
+           cgroupMode: nodeInfo['cgroup-mode'],
+       };
+
+       let rows = {
            memory: {
                header: gettext('Memory'),
                editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
                never_delete: true,
                defaultValue: '512',
                tdCls: 'pve-itype-icon-memory',
-               renderer: function(value, metaData, record) {
-                   var balloon =  me.getObjectValue('balloon');
-                   if (balloon) {
-                       return PVE.Utils.format_size(balloon*1024*1024) + "/" + 
-                           PVE.Utils.format_size(value*1024*1024);
-
-                   } 
-                   return PVE.Utils.format_size(value*1024*1024);
-               }
+               group: 2,
+               multiKey: ['memory', 'balloon', 'shares'],
+               renderer: function(value, metaData, record, ri, ci, store, pending) {
+                   var res = '';
+
+                   var max = me.getObjectValue('memory', 512, pending);
+                   var balloon = me.getObjectValue('balloon', undefined, pending);
+                   var shares = me.getObjectValue('shares', undefined, pending);
+
+                   res = Proxmox.Utils.format_size(max*1024*1024);
+
+                   if (balloon !== undefined && balloon > 0) {
+                       res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res;
+
+                       if (shares) {
+                           res += ' [shares=' + shares +']';
+                       }
+                   } else if (balloon === 0) {
+                       res += ' [balloon=0]';
+                   }
+                   return res;
+               },
            },
            sockets: {
                header: gettext('Processors'),
                never_delete: true,
-               editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ? 
-                   'PVE.qemu.ProcessorEdit' : undefined,
-               tdCls: 'pve-itype-icon-processor',
+               editor: caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']
+                   ? processorEditor : undefined,
+               tdCls: 'pve-itype-icon-cpu',
+               group: 3,
                defaultValue: '1',
                multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
                renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
-
                    var sockets = me.getObjectValue('sockets', 1, pending);
                    var model = me.getObjectValue('cpu', undefined, pending);
                    var cores = me.getObjectValue('cores', 1, pending);
@@ -75,545 +113,649 @@ Ext.define('PVE.qemu.HardwareView', {
                    var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
                    var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
 
-                   var res = Ext.String.format('{0} ({1} sockets, {2} cores)',
-                       sockets*cores, sockets, cores);
+                   let res = Ext.String.format(
+                       '{0} ({1} sockets, {2} cores)', sockets * cores, sockets, cores);
 
                    if (model) {
                        res += ' [' + model + ']';
                    }
-
                    if (numa) {
                        res += ' [numa=' + numa +']';
                    }
-
                    if (vcpus) {
                        res += ' [vcpus=' + vcpus +']';
                    }
-
                    if (cpulimit) {
                        res += ' [cpulimit=' + cpulimit +']';
                    }
-
                    if (cpuunits) {
                        res += ' [cpuunits=' + cpuunits +']';
                    }
 
                    return res;
-               }
+               },
            },
-           keyboard: {
-               header: gettext('Keyboard Layout'),
+           bios: {
+               header: 'BIOS',
+               group: 4,
                never_delete: true,
-               editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.KeyboardEdit' : undefined,
-               tdCls: 'pve-itype-icon-keyboard',
+               editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
                defaultValue: '',
-               renderer: PVE.Utils.render_kvm_language
+               iconCls: 'microchip',
+               renderer: PVE.Utils.render_qemu_bios,
            },
            vga: {
                header: gettext('Display'),
                editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
                never_delete: true,
-               tdCls: 'pve-itype-icon-display',
+               iconCls: 'desktop',
+               group: 5,
+               defaultValue: '',
+               renderer: PVE.Utils.render_kvm_vga_driver,
+           },
+           machine: {
+               header: gettext('Machine'),
+               editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.MachineEdit' : undefined,
+               iconCls: 'cogs',
+               never_delete: true,
+               group: 6,
                defaultValue: '',
-               renderer: PVE.Utils.render_kvm_vga_driver               
+               renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
+                   let ostype = me.getObjectValue('ostype', undefined, pending);
+                   if (PVE.Utils.is_windows(ostype) &&
+                       (!value || value === 'pc' || value === 'q35')) {
+                       return value === 'q35' ? 'pc-q35-5.1' : 'pc-i440fx-5.1';
+                   }
+                   return PVE.Utils.render_qemu_machine(value);
+               },
+           },
+           scsihw: {
+               header: gettext('SCSI Controller'),
+               iconCls: 'database',
+               editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.ScsiHwEdit' : undefined,
+               renderer: PVE.Utils.render_scsihw,
+               group: 7,
+               never_delete: true,
+               defaultValue: '',
+           },
+           vmstate: {
+               header: gettext('Hibernation VM State'),
+               iconCls: 'download',
+               del_extra_msg: gettext('The saved VM state will be permanently lost.'),
+               group: 100,
            },
            cores: {
-               visible: false
+               visible: false,
            },
            cpu: {
-               visible: false
+               visible: false,
            },
            numa: {
-               visible: false
+               visible: false,
            },
            balloon: {
-               visible: false
+               visible: false,
            },
            hotplug: {
-               visible: false
+               visible: false,
            },
            vcpus: {
-               visible: false
+               visible: false,
            },
            cpuunits: {
-               visible: false
+               visible: false,
            },
            cpulimit: {
-               visible: false
+               visible: false,
+           },
+           shares: {
+               visible: false,
+           },
+           ostype: {
+               visible: false,
            },
-           bios: {
-               visible: false
-           }
-
        };
 
-       for (i = 0; i < 4; i++) {
-           confid = "ide" + i.toString();
+       PVE.Utils.forEachBus(undefined, function(type, id) {
+           let confid = type + id;
            rows[confid] = {
-               group: 1,
-               tdCls: 'pve-itype-icon-storage',
+               group: 10,
+               iconCls: 'hdd-o',
                editor: 'PVE.qemu.HDEdit',
-               never_delete: caps.vms['VM.Config.Disk'] ? false : true,
+               isOnStorageBus: true,
                header: gettext('Hard Disk') + ' (' + confid +')',
-               cdheader: gettext('CD/DVD Drive') + ' (' + confid +')'
+               cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
+               cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')',
            };
-       }
-       for (i = 0; i < 6; i++) {
-           confid = "sata" + i.toString();
-           rows[confid] = {
-               group: 1,
-               tdCls: 'pve-itype-icon-storage',
-               editor: 'PVE.qemu.HDEdit',
-               never_delete: caps.vms['VM.Config.Disk'] ? false : true,
-               header: gettext('Hard Disk') + ' (' + confid +')',
-               cdheader: gettext('CD/DVD Drive') + ' (' + confid +')'
-           };
-       }
-       for (i = 0; i < 16; i++) {
-           confid = "scsi" + i.toString();
-           rows[confid] = {
-               group: 1,
-               tdCls: 'pve-itype-icon-storage',
-               editor: 'PVE.qemu.HDEdit',
-               never_delete: caps.vms['VM.Config.Disk'] ? false : true,
-               header: gettext('Hard Disk') + ' (' + confid +')',
-               cdheader: gettext('CD/DVD Drive') + ' (' + confid +')'
-           };
-       }
-       for (i = 0; i < 16; i++) {
-           confid = "virtio" + i.toString();
-           rows[confid] = {
-               group: 1,
-               tdCls: 'pve-itype-icon-storage',
-               editor: 'PVE.qemu.HDEdit',
-               never_delete: caps.vms['VM.Config.Disk'] ? false : true,
-               header: gettext('Hard Disk') + ' (' + confid +')',
-               cdheader: gettext('CD/DVD Drive') + ' (' + confid +')'
-           };
-       }
-       for (i = 0; i < 32; i++) {
-           confid = "net" + i.toString();
+       });
+       for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) {
+           let confid = "net" + i.toString();
            rows[confid] = {
-               group: 2,
-               tdCls: 'pve-itype-icon-network',
+               group: 15,
+               order: i,
+               iconCls: 'exchange',
                editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
-               never_delete: caps.vms['VM.Config.Network'] ? false : true,
-               header: gettext('Network Device') + ' (' + confid +')'
+               never_delete: !caps.vms['VM.Config.Network'],
+               header: gettext('Network Device') + ' (' + confid +')',
            };
        }
        rows.efidisk0 = {
-           group: 3,
-           tdCls: 'pve-itype-icon-storage',
+           group: 20,
+           iconCls: 'hdd-o',
+           editor: null,
+           never_delete: !caps.vms['VM.Config.Disk'],
+           header: gettext('EFI Disk'),
+       };
+       rows.tpmstate0 = {
+           group: 22,
+           iconCls: 'hdd-o',
            editor: null,
-           never_delete: caps.vms['VM.Config.Disk'] ? false : true,
-           header: gettext('EFI Disk')
+           never_delete: !caps.vms['VM.Config.Disk'],
+           header: gettext('TPM State'),
        };
-       for (i = 0; i < 5; i++) {
-           confid = "usb" + i.toString();
+       for (let i = 0; i < PVE.Utils.hardware_counts.usb; i++) {
+           let confid = "usb" + i.toString();
            rows[confid] = {
-               group: 4,
-               tdCls: 'pve-itype-icon-usb',
-               editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
-               never_delete: caps.nodes['Sys.Console'] ? false : true,
-               header: gettext('USB Device') + ' (' + confid + ')'
+               group: 25,
+               order: i,
+               iconCls: 'usb',
+               editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.USBEdit' : undefined,
+               never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
+               header: gettext('USB Device') + ' (' + confid + ')',
            };
        }
-       for (i = 0; i < 4; i++) {
-           confid = "hostpci" + i.toString();
+       for (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
+           let confid = "hostpci" + i.toString();
            rows[confid] = {
-               group: 5,
+               group: 30,
+               order: i,
                tdCls: 'pve-itype-icon-pci',
-               never_delete: caps.nodes['Sys.Console'] ? false : true,
-               header: gettext('PCI Device') + ' (' + confid + ')'
+               never_delete: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
+               editor: caps.nodes['Sys.Console'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.PCIEdit' : undefined,
+               header: gettext('PCI Device') + ' (' + confid + ')',
            };
        }
-       for (i = 0; i < 8; i++) {
+       for (let i = 0; i < PVE.Utils.hardware_counts.serial; i++) {
+           let confid = "serial" + i.toString();
+           rows[confid] = {
+               group: 35,
+               order: i,
+               tdCls: 'pve-itype-icon-serial',
+               never_delete: !caps.nodes['Sys.Console'],
+               header: gettext('Serial Port') + ' (' + confid + ')',
+           };
+       }
+       rows.audio0 = {
+           group: 40,
+           iconCls: 'volume-up',
+           editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.AudioEdit' : undefined,
+           never_delete: !caps.vms['VM.Config.HWType'],
+           header: gettext('Audio Device'),
+       };
+       for (let i = 0; i < 256; i++) {
            rows["unused" + i.toString()] = {
-               group: 6,
-               tdCls: 'pve-itype-icon-storage',
+               group: 99,
+               order: i,
+               iconCls: 'hdd-o',
+               del_extra_msg: gettext('This will permanently erase all data.'),
                editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
-               header: gettext('Unused Disk') + ' ' + i.toString()
+               header: gettext('Unused Disk') + ' ' + i.toString(),
            };
        }
+       rows.rng0 = {
+           group: 45,
+           tdCls: 'pve-itype-icon-die',
+           editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.RNGEdit' : undefined,
+           never_delete: !caps.nodes['Sys.Console'],
+           header: gettext("VirtIO RNG"),
+       };
 
        var sorterFn = function(rec1, rec2) {
            var v1 = rec1.data.key;
            var v2 = rec2.data.key;
            var g1 = rows[v1].group || 0;
            var g2 = rows[v2].group || 0;
-           
-           return (g1 !== g2) ? 
-               (g1 > g2 ? 1 : -1) : (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0));
-       };
-
-       var reload = function() {
-           me.rstore.load();
-       };
-
-       var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
-
-       var sm = Ext.create('Ext.selection.RowModel', {});
+           var order1 = rows[v1].order || 0;
+           var order2 = rows[v2].order || 0;
 
-       var run_editor = function() {
-           var rec = sm.getSelection()[0];
-           if (!rec) {
-               return;
+           if (g1 - g2 !== 0) {
+               return g1 - g2;
            }
 
-           var rowdef = rows[rec.data.key];
-           if (!rowdef.editor) {
-               return;
-           }
-
-           var editor = rowdef.editor;
-           if (rowdef.tdCls == 'pve-itype-icon-storage') {
-               var value = me.getObjectValue(rec.data.key, '', true); 
-               if (value.match(/media=cdrom/)) {
-                   editor = 'PVE.qemu.CDEdit';
-               }
+           if (order1 - order2 !== 0) {
+               return order1 - order2;
            }
 
-           var win;
-
-           if (Ext.isString(editor)) {
-               win = Ext.create(editor, {
-                   pveSelNode: me.pveSelNode,
-                   confid: rec.data.key,
-                   url: '/api2/extjs/' + baseurl
-               });
+           if (v1 > v2) {
+               return 1;
+           } else if (v1 < v2) {
+               return -1;
            } else {
-               var config = Ext.apply({
-                   pveSelNode: me.pveSelNode,
-                   confid: rec.data.key,
-                   url: '/api2/extjs/' + baseurl
-               }, rowdef.editor);
-               win = Ext.createWidget(rowdef.editor.xtype, config);
-               win.load();
+               return 0;
            }
-
-           win.show();
-           win.on('destroy', reload);
        };
 
-       var run_diskthrottle = function() {
-           var rec = sm.getSelection()[0];
-           if (!rec) {
-               return;
-           }
+       let baseurl = `nodes/${nodename}/qemu/${vmid}/config`;
 
-            var win = Ext.create('PVE.qemu.HDThrottle', {
-               pveSelNode: me.pveSelNode,
-               confid: rec.data.key,
-               url: '/api2/extjs/' + baseurl
-           });
+       let sm = Ext.create('Ext.selection.RowModel', {});
 
-           win.show();
-           win.on('destroy', reload);
-       };
-
-       var run_resize = function() {
-           var rec = sm.getSelection()[0];
-           if (!rec) {
+       let run_editor = function() {
+           let rec = sm.getSelection()[0];
+           if (!rec || !rows[rec.data.key]?.editor) {
                return;
            }
+           let rowdef = rows[rec.data.key];
+           let editor = rowdef.editor;
+
+           if (rowdef.isOnStorageBus) {
+               let value = me.getObjectValue(rec.data.key, '', true);
+               if (isCloudInitKey(value)) {
+                   return;
+               } else if (value.match(/media=cdrom/)) {
+                   editor = 'PVE.qemu.CDEdit';
+               } else if (!diskCap) {
+                   return;
+               }
+           }
 
-           var win = Ext.create('PVE.window.HDResize', {
-               disk: rec.data.key,
-               nodename: nodename,
-               vmid: vmid
-           });
-
-           win.show();
-
-           win.on('destroy', reload);
-       };
-
-       var run_cpuoptions = function() {
-           var sockets = me.getObjectValue('sockets', 1);
-           var cores = me.getObjectValue('cores', 1);
-
-           var win = Ext.create('PVE.qemu.CPUOptions', {
-               maxvcpus: sockets * cores,
-               vmid: vmid,
+           let commonOpts = {
+               autoShow: true,
                pveSelNode: me.pveSelNode,
-               url: '/api2/extjs/' + baseurl
-           });
-
-           win.show();
-
-           win.on('destroy', reload);
-       };
+               confid: rec.data.key,
+               url: `/api2/extjs/${baseurl}`,
+               listeners: {
+                   destroy: () => me.reload(),
+               },
+           };
 
-       var run_move = function() {
-           var rec = sm.getSelection()[0];
-           if (!rec) {
-               return;
+           if (Ext.isString(editor)) {
+               Ext.create(editor, commonOpts);
+           } else {
+               let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor));
+               win.load();
            }
-
-           var win = Ext.create('PVE.window.HDMove', {
-               disk: rec.data.key,
-               nodename: nodename,
-               vmid: vmid
-           });
-
-           win.show();
-
-           win.on('destroy', reload);
        };
 
-       var edit_btn = new PVE.button.Button({
+       let edit_btn = new Proxmox.button.Button({
            text: gettext('Edit'),
            selModel: sm,
            disabled: true,
-           handler: run_editor
-        });
+           handler: run_editor,
+       });
 
-       var resize_btn = new PVE.button.Button({
-           text: gettext('Resize disk'),
+       let move_menuitem = new Ext.menu.Item({
+           text: gettext('Move Storage'),
+           tooltip: gettext('Move disk to another storage'),
+           iconCls: 'fa fa-database',
            selModel: sm,
-           disabled: true,
-           handler: run_resize
+           handler: () => {
+               let rec = sm.getSelection()[0];
+               if (!rec) {
+                   return;
+               }
+               Ext.create('PVE.window.HDMove', {
+                   autoShow: true,
+                   disk: rec.data.key,
+                   nodename: nodename,
+                   vmid: vmid,
+                   type: 'qemu',
+                   listeners: {
+                       destroy: () => me.reload(),
+                   },
+               });
+           },
        });
 
-       var move_btn = new PVE.button.Button({
-           text: gettext('Move disk'),
+       let reassign_menuitem = new Ext.menu.Item({
+           text: gettext('Reassign Owner'),
+           tooltip: gettext('Reassign disk to another VM'),
+           iconCls: 'fa fa-desktop',
            selModel: sm,
-           disabled: true,
-           handler: run_move
+           handler: () => {
+               let rec = sm.getSelection()[0];
+               if (!rec) {
+                   return;
+               }
+
+               Ext.create('PVE.window.GuestDiskReassign', {
+                   autoShow: true,
+                   disk: rec.data.key,
+                   nodename: nodename,
+                   vmid: vmid,
+                   type: 'qemu',
+                   listeners: {
+                       destroy: () => me.reload(),
+                   },
+               });
+           },
        });
 
-       var diskthrottle_btn = new PVE.button.Button({
-           text: gettext('Disk Throttle'),
+       let resize_menuitem = new Ext.menu.Item({
+           text: gettext('Resize'),
+           iconCls: 'fa fa-plus',
            selModel: sm,
-           disabled: true,
-           handler: run_diskthrottle
+           handler: () => {
+               let rec = sm.getSelection()[0];
+               if (!rec) {
+                   return;
+               }
+               Ext.create('PVE.window.HDResize', {
+                   autoShow: true,
+                   disk: rec.data.key,
+                   nodename: nodename,
+                   vmid: vmid,
+                   listeners: {
+                       destroy: () => me.reload(),
+                   },
+               });
+           },
        });
 
-       var cpuoptions_btn = new Ext.Button({
-           text: gettext('CPU options'),
-           handler: run_cpuoptions
+       let diskaction_btn = new Proxmox.button.Button({
+           text: gettext('Disk Action'),
+           disabled: true,
+           menu: {
+               items: [
+                   move_menuitem,
+                   reassign_menuitem,
+                   resize_menuitem,
+               ],
+           },
        });
 
-       var remove_btn = new PVE.button.Button({
+
+       let remove_btn = new Proxmox.button.Button({
            text: gettext('Remove'),
+           defaultText: gettext('Remove'),
+           altText: gettext('Detach'),
            selModel: sm,
            disabled: true,
            dangerous: true,
+           RESTMethod: 'PUT',
            confirmMsg: function(rec) {
-               var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'),
-                                           "'" + me.renderKey(rec.data.key, {}, rec) + "'");
-               if (rec.data.key.match(/^unused\d+$/)) {
-                   msg += " " + gettext('This will permanently erase all data.');
+               let warn = gettext('Are you sure you want to remove entry {0}');
+               if (this.text === this.altText) {
+                   warn = gettext('Are you sure you want to detach entry {0}');
                }
+               let rendered = me.renderKey(rec.data.key, {}, rec);
+               let msg = Ext.String.format(warn, `'${rendered}'`);
 
+               if (rows[rec.data.key].del_extra_msg) {
+                   msg += '<br>' + rows[rec.data.key].del_extra_msg;
+               }
                return msg;
            },
-           handler: function(b, e, rec) {
-               PVE.Utils.API2Request({
+           handler: function(btn, e, rec) {
+               Proxmox.Utils.API2Request({
                    url: '/api2/extjs/' + baseurl,
                    waitMsgTarget: me,
-                   method: 'PUT',
+                   method: btn.RESTMethod,
                    params: {
-                       'delete': rec.data.key
+                       'delete': rec.data.key,
                    },
-                   callback: function() {
-                       reload();
+                   callback: () => me.reload(),
+                   failure: response => Ext.Msg.alert('Error', response.htmlStatus),
+                   success: function(response, options) {
+                       if (btn.RESTMethod === 'POST') {
+                           Ext.create('Proxmox.window.TaskProgress', {
+                               autoShow: true,
+                               upid: response.result.data,
+                               listeners: {
+                                   destroy: () => me.reload(),
+                               },
+                           });
+                       }
                    },
-                   failure: function (response, opts) {
-                       Ext.Msg.alert('Error', response.htmlStatus);
-                   }
                });
-           }
+           },
+           listeners: {
+               render: function(btn) {
+                   // hack: calculate the max button width on first display to prevent the whole
+                   // toolbar to move when we switch between the "Remove" and "Detach" labels
+                   var def = btn.getSize().width;
+
+                   btn.setText(btn.altText);
+                   var alt = btn.getSize().width;
+
+                   btn.setText(btn.defaultText);
+
+                   var optimal = alt > def ? alt : def;
+                   btn.setSize({ width: optimal });
+               },
+           },
        });
 
-       var revert_btn = new PVE.button.Button({
-           text: gettext('Revert'),
-           selModel: sm,
-           disabled: true,
-           handler: function(b, e, rec) {
-               var rowdef = me.rows[rec.data.key] || {};
-               var keys = rowdef.multiKey ||  [ rec.data.key ];
-               var revert = keys.join(',');
-               PVE.Utils.API2Request({
-                   url: '/api2/extjs/' + baseurl,
-                   waitMsgTarget: me,
-                   method: 'PUT',
-                   params: {
-                       'revert': revert
-                   },
-                   callback: function() {
-                       reload();
-                   },
-                   failure: function (response, opts) {
-                       Ext.Msg.alert('Error',response.htmlStatus);
-                   }
-               });
-           }
+       let revert_btn = new PVE.button.PendingRevert({
+           apiurl: '/api2/extjs/' + baseurl,
        });
 
-       var efidisk_menuitem = Ext.create('Ext.menu.Item',{
+       let efidisk_menuitem = Ext.create('Ext.menu.Item', {
            text: gettext('EFI Disk'),
-           iconCls: 'pve-itype-icon-storage',
+           iconCls: 'fa fa-fw fa-hdd-o black',
            disabled: !caps.vms['VM.Config.Disk'],
            handler: function() {
+               let { data: bios } = me.rstore.getData().map.bios || {};
 
-               var rstoredata = me.rstore.getData().map;
-               // check if ovmf is configured
-               if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') {
-                   var win = Ext.create('PVE.qemu.EFIDiskEdit', {
-                       url: '/api2/extjs/' + baseurl,
-                       pveSelNode: me.pveSelNode
-                   });
-                   win.on('destroy', reload);
-                   win.show();
-               } else {
-                   Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.'));
-               }
-
-           }
+               Ext.create('PVE.qemu.EFIDiskEdit', {
+                   autoShow: true,
+                   url: '/api2/extjs/' + baseurl,
+                   pveSelNode: me.pveSelNode,
+                   usesEFI: bios?.value === 'ovmf' || bios?.pending === 'ovmf',
+                   listeners: {
+                       destroy: () => me.reload(),
+                   },
+               });
+           },
        });
 
-       var set_button_status = function() {
-           var sm = me.getSelectionModel();
-           var rec = sm.getSelection()[0];
-
-           // disable button when we have an efidisk already
-           // disable is ok in this case, because you can instantly
-           // see that there is already one
-           efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined);
-           // en/disable usb add button
-           var count = 0;
-           me.rstore.getData().items.forEach(function(item){
-               if (/^usb\d+/.test(item.id)) {
-                   count++;
+       let counts = {};
+       let isAtLimit = (type) => counts[type] >= PVE.Utils.hardware_counts[type];
+       let isAtUsbLimit = () => {
+           let ostype = me.getObjectValue('ostype');
+           let machine = me.getObjectValue('machine');
+           return counts.usb >= PVE.Utils.get_max_usb_count(ostype, machine);
+       };
+
+       let set_button_status = function() {
+           let selection_model = me.getSelectionModel();
+           let rec = selection_model.getSelection()[0];
+
+           counts = {}; // en/disable hardwarebuttons
+           let hasCloudInit = false;
+           me.rstore.getData().items.forEach(function({ id, data }) {
+               if (!hasCloudInit && (isCloudInitKey(data.value) || isCloudInitKey(data.pending))) {
+                   hasCloudInit = true;
+                   return;
+               }
+
+               let match = id.match(/^([^\d]+)\d+$/);
+               if (match && PVE.Utils.hardware_counts[match[1]] !== undefined) {
+                   let type = match[1];
+                   counts[type] = (counts[type] || 0) + 1;
                }
            });
-           me.down('#addusb').setDisabled((count >= 5));
+
+           // heuristic only for disabling some stuff, the backend has the final word.
+           const noSysConsolePerm = !caps.nodes['Sys.Console'];
+           const noHWPerm = !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'];
+           const noVMConfigHWTypePerm = !caps.vms['VM.Config.HWType'];
+           const noVMConfigNetPerm = !caps.vms['VM.Config.Network'];
+           const noVMConfigDiskPerm = !caps.vms['VM.Config.Disk'];
+           const noVMConfigCDROMPerm = !caps.vms['VM.Config.CDROM'];
+           const noVMConfigCloudinitPerm = !caps.vms['VM.Config.Cloudinit'];
+
+           me.down('#addUsb').setDisabled(noHWPerm || isAtUsbLimit());
+           me.down('#addPci').setDisabled(noHWPerm || isAtLimit('hostpci'));
+           me.down('#addAudio').setDisabled(noVMConfigHWTypePerm || isAtLimit('audio'));
+           me.down('#addSerial').setDisabled(noVMConfigHWTypePerm || isAtLimit('serial'));
+           me.down('#addNet').setDisabled(noVMConfigNetPerm || isAtLimit('net'));
+           me.down('#addRng').setDisabled(noSysConsolePerm || isAtLimit('rng'));
+           efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk'));
+           me.down('#addTpmState').setDisabled(noSysConsolePerm || isAtLimit('tpmstate'));
+           me.down('#addCloudinitDrive').setDisabled(noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit);
 
            if (!rec) {
                remove_btn.disable();
                edit_btn.disable();
-               resize_btn.disable();
-               move_btn.disable();
-               diskthrottle_btn.disable();
+               diskaction_btn.disable();
                revert_btn.disable();
                return;
            }
-           var key = rec.data.key;
-           var value = rec.data.value;
-           var rowdef = rows[key];
-
-           var pending = rec.data['delete'] || me.hasPendingChanges(key);
-           var isDisk = !key.match(/^unused\d+/) &&
-               rowdef.tdCls == 'pve-itype-icon-storage' &&
-               (value && !value.match(/media=cdrom/));
-
-           var isEfi = (key === 'efidisk0');
-
-
-           remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true));
-
-           edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor);
-
-           resize_btn.setDisabled(pending || !isDisk);
-
-           move_btn.setDisabled(pending || !isDisk);
-
-           diskthrottle_btn.setDisabled(pending || !isDisk || isEfi);
+           const { key, value } = rec.data;
+           const row = rows[key];
+
+           const deleted = !!rec.data.delete;
+           const pending = deleted || me.hasPendingChanges(key);
+
+           const isCloudInit = isCloudInitKey(value);
+           const isCDRom = value && !!value.toString().match(/media=cdrom/);
+
+           const isUnusedDisk = key.match(/^unused\d+/);
+           const isUsedDisk = !isUnusedDisk && row.isOnStorageBus && !isCDRom;
+           const isDisk = isUnusedDisk || isUsedDisk;
+           const isEfi = key === 'efidisk0';
+           const tpmMoveable = key === 'tpmstate0' && !me.pveSelNode.data.running;
+
+           let cannotDelete = deleted || row.never_delete;
+           cannotDelete ||= isCDRom && !cdromCap;
+           cannotDelete ||= isDisk && !diskCap;
+           cannotDelete ||= isCloudInit && noVMConfigCloudinitPerm;
+           remove_btn.setDisabled(cannotDelete);
+
+           remove_btn.setText(isUsedDisk && !isCloudInit ? remove_btn.altText : remove_btn.defaultText);
+           remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
+
+           edit_btn.setDisabled(
+               deleted || !row.editor || isCloudInit || (isCDRom && !cdromCap) || (isDisk && !diskCap));
+
+           diskaction_btn.setDisabled(
+               pending ||
+               !diskCap ||
+               isCloudInit ||
+               !(isDisk || isEfi || tpmMoveable),
+           );
+           reassign_menuitem.setDisabled(pending || (isEfi || tpmMoveable));
+           resize_menuitem.setDisabled(pending || !isUsedDisk);
 
            revert_btn.setDisabled(!pending);
+       };
 
+       let editorFactory = (classPath, extraOptions) => {
+           extraOptions = extraOptions || {};
+           return () => Ext.create(`PVE.qemu.${classPath}`, {
+               autoShow: true,
+               url: `/api2/extjs/${baseurl}`,
+               pveSelNode: me.pveSelNode,
+               listeners: {
+                   destroy: () => me.reload(),
+               },
+               isAdd: true,
+               isCreate: true,
+               ...extraOptions,
+           });
        };
 
        Ext.apply(me, {
-           url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending',
+           url: `/api2/json/nodes/${nodename}/qemu/${vmid}/pending`,
            interval: 5000,
            selModel: sm,
-           tbar: [ 
+           run_editor: run_editor,
+           tbar: [
                {
                    text: gettext('Add'),
                    menu: new Ext.menu.Menu({
+                       cls: 'pve-add-hw-menu',
                        items: [
                            {
                                text: gettext('Hard Disk'),
-                               iconCls: 'pve-itype-icon-storage',
+                               iconCls: 'fa fa-fw fa-hdd-o black',
                                disabled: !caps.vms['VM.Config.Disk'],
-                               handler: function() {
-                                   var win = Ext.create('PVE.qemu.HDEdit', {
-                                       url: '/api2/extjs/' + baseurl,
-                                       pveSelNode: me.pveSelNode
-                                   });
-                                   win.on('destroy', reload);
-                                   win.show();
-                               }
+                               handler: editorFactory('HDEdit'),
                            },
                            {
                                text: gettext('CD/DVD Drive'),
                                iconCls: 'pve-itype-icon-cdrom',
-                               disabled: !caps.vms['VM.Config.Disk'],
-                               handler: function() {
-                                   var win = Ext.create('PVE.qemu.CDEdit', {
-                                       url: '/api2/extjs/' + baseurl,
-                                       pveSelNode: me.pveSelNode
-                                   });
-                                   win.on('destroy', reload);
-                                   win.show();
-                               }
+                               disabled: !caps.vms['VM.Config.CDROM'],
+                               handler: editorFactory('CDEdit'),
                            },
                            {
                                text: gettext('Network Device'),
-                               iconCls: 'pve-itype-icon-network',
+                               itemId: 'addNet',
+                               iconCls: 'fa fa-fw fa-exchange black',
                                disabled: !caps.vms['VM.Config.Network'],
-                               handler: function() {
-                                   var win = Ext.create('PVE.qemu.NetworkEdit', {
-                                       url: '/api2/extjs/' + baseurl,
-                                       pveSelNode: me.pveSelNode
-                                   });
-                                   win.on('destroy', reload);
-                                   win.show();
-                               }
+                               handler: editorFactory('NetworkEdit'),
                            },
                            efidisk_menuitem,
+                           {
+                               text: gettext('TPM State'),
+                               itemId: 'addTpmState',
+                               iconCls: 'fa fa-fw fa-hdd-o black',
+                               disabled: !caps.vms['VM.Config.Disk'],
+                               handler: editorFactory('TPMDiskEdit'),
+                           },
                            {
                                text: gettext('USB Device'),
-                               itemId: 'addusb',
-                               iconCls: 'pve-itype-icon-usb',
+                               itemId: 'addUsb',
+                               iconCls: 'fa fa-fw fa-usb black',
+                               disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
+                               handler: editorFactory('USBEdit'),
+                           },
+                           {
+                               text: gettext('PCI Device'),
+                               itemId: 'addPci',
+                               iconCls: 'pve-itype-icon-pci',
+                               disabled: !caps.nodes['Sys.Console'] && !caps.mapping['Mapping.Use'],
+                               handler: editorFactory('PCIEdit'),
+                           },
+                           {
+                               text: gettext('Serial Port'),
+                               itemId: 'addSerial',
+                               iconCls: 'pve-itype-icon-serial',
+                               disabled: !caps.vms['VM.Config.Options'],
+                               handler: editorFactory('SerialEdit'),
+                           },
+                           {
+                               text: gettext('CloudInit Drive'),
+                               itemId: 'addCloudinitDrive',
+                               iconCls: 'fa fa-fw fa-cloud black',
+                               disabled: !caps.vms['VM.Config.CDROM'] || !caps.vms['VM.Config.Cloudinit'],
+                               handler: editorFactory('CIDriveEdit'),
+                           },
+                           {
+                               text: gettext('Audio Device'),
+                               itemId: 'addAudio',
+                               iconCls: 'fa fa-fw fa-volume-up black',
+                               disabled: !caps.vms['VM.Config.HWType'],
+                               handler: editorFactory('AudioEdit'),
+                           },
+                           {
+                               text: gettext("VirtIO RNG"),
+                               itemId: 'addRng',
+                               iconCls: 'pve-itype-icon-die',
                                disabled: !caps.nodes['Sys.Console'],
-                               handler: function() {
-                                   var win = Ext.create('PVE.qemu.USBEdit', {
-                                       url: '/api2/extjs/' + baseurl,
-                                       pveSelNode: me.pveSelNode
-                                   });
-                                   win.on('destroy', reload);
-                                   win.show();
-                               }
-                           }
-                       ]
-                   })
+                               handler: editorFactory('RNGEdit'),
+                           },
+                       ],
+                   }),
                },
                remove_btn,
                edit_btn,
-               resize_btn,
-               move_btn,
-               diskthrottle_btn,
-               cpuoptions_btn,
-               revert_btn
+               diskaction_btn,
+               revert_btn,
            ],
            rows: rows,
            sorterFn: sorterFn,
            listeners: {
                itemdblclick: run_editor,
-               selectionchange: set_button_status
-           }
+               selectionchange: set_button_status,
+           },
        });
 
        me.callParent();
 
-       me.on('activate', me.rstore.startUpdate);
-       me.on('destroy', me.rstore.stopUpdate); 
+       me.on('activate', me.rstore.startUpdate, me.rstore);
+       me.on('destroy', me.rstore.stopUpdate, me.rstore);
 
-       me.mon(me.rstore, 'refresh', function() {
-           set_button_status();
-       });
-    }
+       me.mon(me.getStore(), 'datachanged', set_button_status, me);
+    },
 });