Ext.define('PVE.qemu.HardwareView', {
    extend: 'Proxmox.grid.PendingObjectGrid',
    alias: ['widget.PVE.qemu.HardwareView'],

    onlineHelp: 'qm_virtual_machines_settings',

    renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
	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;
	} 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;
	}
    },

    initComponent: function() {
	var me = this;

	const { node: nodename, vmid } = me.pveSelNode.data;
	if (!nodename) {
	    throw "no node name specified";
	} else if (!vmid) {
	    throw "no VM ID specified";
	}

	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/);

	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',
		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']
		    ? processorEditor : undefined,
		tdCls: 'pve-itype-icon-cpu',
		group: 3,
		defaultValue: '1',
		multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits', 'affinity'],
		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);
		    var numa = me.getObjectValue('numa', undefined, pending);
		    var vcpus = me.getObjectValue('vcpus', undefined, pending);
		    var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
		    var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
		    var cpuaffinity = me.getObjectValue('affinity', undefined, pending);

		    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 +']';
		    }
		    if (cpuaffinity) {
			res += ' [cpuaffinity=' + cpuaffinity + ']';
		    }

		    return res;
		},
	    },
	    bios: {
		header: 'BIOS',
		group: 4,
		never_delete: true,
		editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.BiosEdit' : undefined,
		defaultValue: '',
		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,
		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: 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' : 'virt';
		    }
		    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,
	    },
	    cpu: {
		visible: false,
	    },
	    numa: {
		visible: false,
	    },
	    balloon: {
		visible: false,
	    },
	    hotplug: {
		visible: false,
	    },
	    vcpus: {
		visible: false,
	    },
	    cpuunits: {
		visible: false,
	    },
	    cpulimit: {
		visible: false,
	    },
	    shares: {
		visible: false,
	    },
	    ostype: {
		visible: false,
	    },
	    affinity: {
		visible: false,
	    },
	};

	PVE.Utils.forEachBus(undefined, function(type, id) {
	    let confid = type + id;
	    rows[confid] = {
		group: 10,
		iconCls: 'hdd-o',
		editor: 'PVE.qemu.HDEdit',
		isOnStorageBus: true,
		header: gettext('Hard Disk') + ' (' + confid +')',
		cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
		cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')',
	    };
	});
	for (let i = 0; i < PVE.Utils.hardware_counts.net; i++) {
	    let confid = "net" + i.toString();
	    rows[confid] = {
		group: 15,
		order: i,
		iconCls: 'exchange',
		editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
		never_delete: !caps.vms['VM.Config.Network'],
		header: gettext('Network Device') + ' (' + confid +')',
	    };
	}
	rows.efidisk0 = {
	    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'],
	    header: gettext('TPM State'),
	};
	for (let i = 0; i < PVE.Utils.hardware_counts.usb; i++) {
	    let confid = "usb" + i.toString();
	    rows[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 (let i = 0; i < PVE.Utils.hardware_counts.hostpci; i++) {
	    let confid = "hostpci" + i.toString();
	    rows[confid] = {
		group: 30,
		order: i,
		tdCls: 'pve-itype-icon-pci',
		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 (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: 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(),
	    };
	}
	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;
	    var order1 = rows[v1].order || 0;
	    var order2 = rows[v2].order || 0;

	    if (g1 - g2 !== 0) {
		return g1 - g2;
	    }

	    if (order1 - order2 !== 0) {
		return order1 - order2;
	    }

	    if (v1 > v2) {
		return 1;
	    } else if (v1 < v2) {
	        return -1;
	    } else {
		return 0;
	    }
	};

	let baseurl = `nodes/${nodename}/qemu/${vmid}/config`;

	let sm = Ext.create('Ext.selection.RowModel', {});

	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;
		}
	    }

	    let commonOpts = {
		autoShow: true,
		pveSelNode: me.pveSelNode,
		confid: rec.data.key,
		url: `/api2/extjs/${baseurl}`,
		listeners: {
		    destroy: () => me.reload(),
		},
	    };

	    if (Ext.isString(editor)) {
		Ext.create(editor, commonOpts);
	    } else {
		let win = Ext.createWidget(rowdef.editor.xtype, Ext.apply(commonOpts, rowdef.editor));
		win.load();
	    }
	};

	let edit_btn = new Proxmox.button.Button({
	    text: gettext('Edit'),
	    selModel: sm,
	    disabled: true,
	    handler: run_editor,
	});

	let move_menuitem = new Ext.menu.Item({
	    text: gettext('Move Storage'),
	    tooltip: gettext('Move disk to another storage'),
	    iconCls: 'fa fa-database',
	    selModel: sm,
	    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(),
		    },
		});
	    },
	});

	let reassign_menuitem = new Ext.menu.Item({
	    text: gettext('Reassign Owner'),
	    tooltip: gettext('Reassign disk to another VM'),
	    iconCls: 'fa fa-desktop',
	    selModel: sm,
	    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(),
		    },
		});
	    },
	});

	let resize_menuitem = new Ext.menu.Item({
	    text: gettext('Resize'),
	    iconCls: 'fa fa-plus',
	    selModel: sm,
	    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(),
		    },
		});
	    },
	});

	let diskaction_btn = new Proxmox.button.Button({
	    text: gettext('Disk Action'),
	    disabled: true,
	    menu: {
		items: [
		    move_menuitem,
		    reassign_menuitem,
		    resize_menuitem,
		],
	    },
	});


	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) {
		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(btn, e, rec) {
		let params = { 'delete': rec.data.key };
		if (btn.RESTMethod === 'POST') {
		    params.background_delay = 5;
		}
		Proxmox.Utils.API2Request({
		    url: '/api2/extjs/' + baseurl,
		    waitMsgTarget: me,
		    method: btn.RESTMethod,
		    params: params,
		    callback: () => me.reload(),
		    failure: response => Ext.Msg.alert('Error', response.htmlStatus),
		    success: function(response, options) {
			if (btn.RESTMethod === 'POST' && response.result.data !== null) {
			    Ext.create('Proxmox.window.TaskProgress', {
				autoShow: true,
				upid: response.result.data,
				listeners: {
				    destroy: () => me.reload(),
				},
			    });
			}
		    },
		});
	    },
	    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 });
		},
	    },
	});

	let revert_btn = new PVE.button.PendingRevert({
	    apiurl: '/api2/extjs/' + baseurl,
	});

	let efidisk_menuitem = Ext.create('Ext.menu.Item', {
	    text: gettext('EFI Disk'),
	    iconCls: 'fa fa-fw fa-hdd-o black',
	    disabled: !caps.vms['VM.Config.Disk'],
	    handler: function() {
		let { data: bios } = me.rstore.getData().map.bios || {};

		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(),
		    },
		});
	    },
	});

	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;
		}
	    });

	    // 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(noVMConfigDiskPerm || isAtLimit('tpmstate'));
	    me.down('#addCloudinitDrive').setDisabled(noVMConfigCDROMPerm || noVMConfigCloudinitPerm || hasCloudInit);

	    if (!rec) {
		remove_btn.disable();
		edit_btn.disable();
		diskaction_btn.disable();
		revert_btn.disable();
		return;
	    }
	    const { key, value } = rec.data;
	    const row = rows[key];

	    const deleted = !!rec.data.delete;
	    const pending = deleted || me.hasPendingChanges(key);
	    const isRunning = me.pveSelNode.data.running;

	    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' && !isRunning;

	    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 || (isDisk && isRunning) ? '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`,
	    interval: 5000,
	    selModel: sm,
	    run_editor: run_editor,
	    tbar: [
		{
		    text: gettext('Add'),
		    menu: new Ext.menu.Menu({
			cls: 'pve-add-hw-menu',
			items: [
			    {
				text: gettext('Hard Disk'),
				iconCls: 'fa fa-fw fa-hdd-o black',
				disabled: !caps.vms['VM.Config.Disk'],
				handler: editorFactory('HDEdit'),
			    },
			    {
				text: gettext('CD/DVD Drive'),
				iconCls: 'pve-itype-icon-cdrom',
				disabled: !caps.vms['VM.Config.CDROM'],
				handler: editorFactory('CDEdit'),
			    },
			    {
				text: gettext('Network Device'),
				itemId: 'addNet',
				iconCls: 'fa fa-fw fa-exchange black',
				disabled: !caps.vms['VM.Config.Network'],
				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: '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: editorFactory('RNGEdit'),
			    },
			],
		    }),
		},
		remove_btn,
		edit_btn,
		diskaction_btn,
		revert_btn,
	    ],
	    rows: rows,
	    sorterFn: sorterFn,
	    listeners: {
		itemdblclick: run_editor,
		selectionchange: set_button_status,
	    },
	});

	me.callParent();

	me.on('activate', me.rstore.startUpdate, me.rstore);
	me.on('destroy', me.rstore.stopUpdate, me.rstore);

	me.mon(me.getStore(), 'datachanged', set_button_status, me);
    },
});
