Ext.define('PVE.form.MultiPCISelector', { extend: 'Ext.grid.Panel', alias: 'widget.pveMultiPCISelector', emptyText: gettext('No Devices found'), mixins: { field: 'Ext.form.field.Field', }, getValue: function() { let me = this; return me.value ?? []; }, getSubmitData: function() { let me = this; let res = {}; res[me.name] = me.getValue(); return res; }, setValue: function(value) { let me = this; value ??= []; me.updateSelectedDevices(value); return me.mixins.field.setValue.call(me, value); }, getErrors: function() { let me = this; let errorCls = ['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']; if (me.getValue().length < 1) { let error = gettext("Must choose at least one device"); me.addCls(errorCls); me.getActionEl()?.dom.setAttribute('data-errorqtip', error); return [error]; } me.removeCls(errorCls); me.getActionEl()?.dom.setAttribute('data-errorqtip', ""); return []; }, viewConfig: { getRowClass: function(record) { if (record.data.disabled === true) { return 'x-item-disabled'; } return ''; }, }, updateSelectedDevices: function(value = []) { let me = this; let recs = []; let store = me.getStore(); for (const map of value) { let parsed = PVE.Parser.parsePropertyString(map); if (parsed.node !== me.nodename) { continue; } let rec = store.getById(parsed.path); if (rec) { recs.push(rec); } } me.suspendEvent('change'); me.setSelection(); me.setSelection(recs); me.resumeEvent('change'); }, setNodename: function(nodename) { let me = this; if (!nodename || me.nodename === nodename) { return; } me.nodename = nodename; me.getStore().setProxy({ type: 'proxmox', url: '/api2/json/nodes/' + me.nodename + '/hardware/pci?pci-class-blacklist=', }); me.setSelection(); me.getStore().load({ callback: (recs, op, success) => me.addSlotRecords(recs, op, success), }); }, setMdev: function(mdev) { let me = this; if (mdev) { me.getStore().addFilter({ id: 'mdev-filter', property: 'mdev', value: '1', operator: '=', }); } else { me.getStore().removeFilter('mdev-filter'); } me.setSelection(); }, // adds the virtual 'slot' records (e.g. '0000:01:00') to the store addSlotRecords: function(records, _op, success) { let me = this; if (!success) { return; } let slots = {}; records.forEach((rec) => { let slotname = rec.data.id.slice(0, -2); // remove function if (slots[slotname] !== undefined) { slots[slotname].count++; rec.set('slot', slots[slotname]); return; } slots[slotname] = { count: 1, }; rec.set('slot', slots[slotname]); if (rec.data.id.endsWith('.0')) { slots[slotname].device = rec.data; } }); let store = me.getStore(); for (const [slot, { count, device }] of Object.entries(slots)) { if (count === 1) { continue; } store.add(Ext.apply({}, { id: slot, mdev: undefined, device_name: gettext('Pass through all functions as one device'), }, device)); } me.updateSelectedDevices(me.value); }, selectionChange: function(_grid, selection) { let me = this; let ids = {}; selection .filter(rec => rec.data.id.indexOf('.') === -1) .forEach((rec) => { ids[rec.data.id] = true; }); let to_disable = []; me.getStore().each(rec => { let id = rec.data.id; rec.set('disabled', false); if (id.indexOf('.') === -1) { return; } let slot = id.slice(0, -2); // remove function if (ids[slot]) { to_disable.push(rec); rec.set('disabled', true); } }); me.suspendEvent('selectionchange'); me.getSelectionModel().deselect(to_disable); me.resumeEvent('selectionchange'); me.value = me.getSelection().map((rec) => { let res = { path: rec.data.id, node: me.nodename, id: `${rec.data.vendor}:${rec.data.device}`.replace(/0x/g, ''), 'subsystem-id': `${rec.data.subsystem_vendor}:${rec.data.subsystem_device}`.replace(/0x/g, ''), }; if (rec.data.iommugroup !== -1) { res.iommugroup = rec.data.iommugroup; } return PVE.Parser.printPropertyString(res); }); me.checkChange(); }, selModel: { type: 'checkboxmodel', mode: 'SIMPLE', }, columns: [ { header: 'ID', dataIndex: 'id', renderer: function(value, _md, rec) { if (value.match(/\.[0-9a-f]/i) && rec.data.slot?.count > 1) { return ` ${value}`; } return value; }, width: 150, }, { header: gettext('IOMMU Group'), dataIndex: 'iommugroup', renderer: (v, _md, rec) => rec.data.slot === rec.data.id ? '' : v === -1 ? '-' : v, width: 50, }, { header: gettext('Vendor'), dataIndex: 'vendor_name', flex: 3, }, { header: gettext('Device'), dataIndex: 'device_name', flex: 6, }, { header: gettext('Mediated Devices'), dataIndex: 'mdev', flex: 1, renderer: function(val) { return Proxmox.Utils.format_boolean(!!val); }, }, ], listeners: { selectionchange: function() { this.selectionChange(...arguments); }, }, store: { fields: [ 'id', 'vendor_name', 'device_name', 'vendor', 'device', 'iommugroup', 'mdev', 'subsystem_vendor', 'subsystem_device', 'disabled', { name: 'subsystem-vendor', calculate: function(data) { return data.subsystem_vendor; }, }, { name: 'subsystem-device', calculate: function(data) { return data.subsystem_device; }, }, ], sorters: [ { property: 'id', direction: 'ASC', }, ], }, initComponent: function() { let me = this; let nodename = me.nodename; me.nodename = undefined; me.callParent(); Proxmox.Utils.monStoreErrors(me, me.getStore(), true); me.setNodename(nodename); me.initField(); }, });