diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css index a9ead5d3..3af64255 100644 --- a/www/css/ext6-pve.css +++ b/www/css/ext6-pve.css @@ -700,3 +700,7 @@ table.osds td:first-of-type { cursor: pointer; padding-left: 2px; } + +.x-grid-item .x-item-disabled { + opacity: 0.3; +} diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 40a60639..e534cecd 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -46,6 +46,7 @@ JSSRC= \ form/IPRefSelector.js \ form/MDevSelector.js \ form/MemoryField.js \ + form/MultiPCISelector.js \ form/NetworkCardSelector.js \ form/NodeSelector.js \ form/PCISelector.js \ diff --git a/www/manager6/form/MultiPCISelector.js b/www/manager6/form/MultiPCISelector.js new file mode 100644 index 00000000..99f9d50b --- /dev/null +++ b/www/manager6/form/MultiPCISelector.js @@ -0,0 +1,288 @@ +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'); + } + }, + + // 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 + rec.set('slot', slotname); + if (slots[slotname] !== undefined) { + slots[slotname].count++; + return; + } + + slots[slotname] = { + count: 1, + }; + + 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', + 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(); + }, +});