pve-manager/www/manager6/qemu/HardwareView.js
Stoiko Ivanov 5641693298 ui: qemu: unify enabled state of pci, usb and rng
all 3 additions fail in the backend, if a user does not have
VM.Config.HWType on the VM.

unify their display to cause less confusion, to the most recently
added version for the rng.

drop the now unused noHWPerm (eslint reminded me about that).

found while reviewing:
https://lore.proxmox.com/all/20250408195959.0b1f3aaf@rosa.proxmox.com/T/#t

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
Link: https://lore.proxmox.com/20250408181640.2332561-1-s.ivanov@proxmox.com
2025-04-09 08:01:53 +02:00

793 lines
22 KiB
JavaScript

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' : '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,
},
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 + ')',
renderer: Ext.htmlEncode,
};
});
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'),
renderer: Ext.htmlEncode,
};
rows.tpmstate0 = {
group: 22,
iconCls: 'hdd-o',
editor: null,
never_delete: !caps.vms['VM.Config.Disk'],
header: gettext('TPM State'),
renderer: Ext.htmlEncode,
};
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(),
renderer: Ext.htmlEncode,
};
}
rows.rng0 = {
group: 45,
tdCls: 'pve-itype-icon-die',
editor: caps.vms['VM.Config.HWType'] || caps.mapping['Mapping.Use'] ? 'PVE.qemu.RNGEdit' : undefined,
never_delete: !caps.vms['VM.Config.HWType'] && !caps.mapping['Mapping.Use'],
header: gettext("VirtIO RNG"),
};
for (let i = 0; i < PVE.Utils.hardware_counts.virtiofs; i++) {
let confid = "virtiofs" + i.toString();
rows[confid] = {
group: 50,
order: i,
iconCls: 'folder',
editor: 'PVE.qemu.VirtiofsEdit',
header: gettext('Virtiofs') + ' (' + confid +')',
};
}
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 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'];
const noVMConfigOptionsPerm = !caps.vms['VM.Config.Options'];
me.down('#addUsb').setDisabled(noVMConfigHWTypePerm || isAtUsbLimit());
me.down('#addPci').setDisabled(noVMConfigHWTypePerm || 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(noVMConfigHWTypePerm || isAtLimit('rng'));
efidisk_menuitem.setDisabled(noVMConfigDiskPerm || isAtLimit('efidisk'));
me.down('#addTpmState').setDisabled(noVMConfigDiskPerm || isAtLimit('tpmstate'));
me.down('#addVirtiofs').setDisabled(noVMConfigOptionsPerm || isAtLimit('virtiofs'));
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.vms['VM.Config.HWType'] && !caps.mapping['Mapping.Use'],
handler: editorFactory('RNGEdit'),
},
{
text: gettext("Virtiofs"),
itemId: 'addVirtiofs',
iconCls: 'fa fa-folder',
disabled: !caps.nodes['Sys.Console'],
handler: editorFactory('VirtiofsEdit'),
},
],
}),
},
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);
},
});