[pve-devel] [PATCH v5 09/15] ui: form: add MultiPCISelector

Dominik Csapak d.csapak at proxmox.com
Tue Jun 6 15:52:16 CEST 2023


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 at proxmox.com>
---
 www/css/ext6-pve.css                  |   4 +
 www/manager6/Makefile                 |   1 +
 www/manager6/form/MultiPCISelector.js | 288 ++++++++++++++++++++++++++
 3 files changed, 293 insertions(+)
 create mode 100644 www/manager6/form/MultiPCISelector.js

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 4ffee419..777089b4 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();
+    },
+});
-- 
2.30.2






More information about the pve-devel mailing list