]> git.proxmox.com Git - pve-manager.git/commitdiff
ui: form: add MultiPCISelector
authorDominik Csapak <d.csapak@proxmox.com>
Fri, 16 Jun 2023 13:05:35 +0000 (15:05 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Fri, 16 Jun 2023 14:25:42 +0000 (16:25 +0200)
this is a grid field for selecting multiple pci devices at once, like we
need for the mapped pci ui. There we want to be able to select multiple
devices such that one gets selected automatically

we can select a whole slot here, but that disables selecting the
individual functions of that device.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
www/css/ext6-pve.css
www/manager6/Makefile
www/manager6/form/MultiPCISelector.js [new file with mode: 0644]

index a9ead5d3bc12e23bef5e4767cd3bc009b935ffc3..3af642553d055e08da20d53e51821f5ac8072fdd 100644 (file)
@@ -700,3 +700,7 @@ table.osds td:first-of-type {
     cursor: pointer;
     padding-left: 2px;
 }
+
+.x-grid-item .x-item-disabled {
+    opacity: 0.3;
+}
index 40a606392de0963f6c37198cf46137ce70e29061..e534cecd6563e5a22c64af364d00870ccd957503 100644 (file)
@@ -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 (file)
index 0000000..99f9d50
--- /dev/null
@@ -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();
+    },
+});