[pve-devel] [PATCH manager v3 11/13] ui: add dc/HardwareView: a CRUD interface for hardware mapping

Dominik Csapak d.csapak at proxmox.com
Tue Sep 20 14:50:39 CEST 2022


it's possible to add/edit/remove mappings here, with a cluster
wide view on the mappings and validity.

to do that, we have to to an api call for each node, since
we don't have the pci status synced across them.

Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
 www/manager6/Makefile           |   1 +
 www/manager6/dc/Config.js       |  18 +-
 www/manager6/dc/HardwareView.js | 324 ++++++++++++++++++++++++++++++++
 3 files changed, 341 insertions(+), 2 deletions(-)
 create mode 100644 www/manager6/dc/HardwareView.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 5507276e..869395e1 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -163,6 +163,7 @@ JSSRC= 							\
 	dc/UserEdit.js					\
 	dc/UserView.js					\
 	dc/MetricServerView.js				\
+	dc/HardwareView.js				\
 	lxc/CmdMenu.js					\
 	lxc/Config.js					\
 	lxc/CreateWizard.js				\
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 13ded12e..37148588 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -255,8 +255,22 @@ Ext.define('PVE.dc.Config', {
 		iconCls: 'fa fa-bar-chart',
 		itemId: 'metricservers',
 		onlineHelp: 'external_metric_server',
-	    },
-	    {
+	    });
+	}
+
+	if (caps.hardware['Hardware.Use'] ||
+	    caps.hardware['Hardware.Audit'] ||
+	    caps.hardware['Hardware.Configure']) {
+	    me.items.push({
+		xtype: 'pveDcHardwareView',
+		title: gettext('Hardware'),
+		iconCls: 'fa fa-desktop',
+		itemId: 'hardware',
+	    });
+	}
+
+	if (caps.dc['Sys.Audit']) {
+	    me.items.push({
 		xtype: 'pveDcSupport',
 		title: gettext('Support'),
 		itemId: 'support',
diff --git a/www/manager6/dc/HardwareView.js b/www/manager6/dc/HardwareView.js
new file mode 100644
index 00000000..7201d70f
--- /dev/null
+++ b/www/manager6/dc/HardwareView.js
@@ -0,0 +1,324 @@
+Ext.define('pve-hardware-tree', {
+    extend: 'Ext.data.Model',
+    fields: ['type', 'text', 'path', 'ntype',
+	{
+	    name: 'vendor',
+	    type: 'string',
+	},
+	{
+	    name: 'device',
+	    type: 'string',
+	},
+	{
+	    name: 'iconCls',
+	    calculate: function(data) {
+		if (data.ntype === 'entry') {
+		    if (data.type === 'usb') {
+			return 'fa fa-fw fa-usb';
+		    }
+		    if (data.type === 'pci') {
+			return 'pve-itype-icon-pci';
+		    }
+		    return 'fa fa-fw fa-folder-o';
+		}
+
+		return 'fa fa-fw fa-building';
+	    },
+	},
+	{
+	    name: 'leaf',
+	    calculate: function(data) {
+		return data.ntype && data.ntype !== 'entry';
+	    },
+	},
+    ],
+
+});
+
+Ext.define('PVE.dc.HardwareView', {
+    extend: 'Ext.tree.Panel',
+    alias: 'widget.pveDcHardwareView',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    rootVisible: false,
+
+    cbindData: function(initialConfig) {
+	let me = this;
+	const caps = Ext.state.Manager.get('GuiCap');
+	me.canConfigure = !!caps.nodes['Sys.Modify'] && !!caps.hardware['Hardware.Configure'];
+
+	return {};
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	addPCI: function() {
+	    let me = this;
+	    let nodename = Proxmox.NodeName;
+	    Ext.create('PVE.node.PCIEditWindow', {
+		url: `/nodes/${nodename}/hardware/mapping/pci`,
+		autoShow: true,
+		listeners: {
+		    destroy: () => me.load(),
+		},
+	    });
+	},
+
+	addUSB: function() {
+	    let me = this;
+	    let nodename = Proxmox.NodeName;
+	    Ext.create('PVE.node.USBEditWindow', {
+		url: `/nodes/${nodename}/hardware/mapping/usb`,
+		autoShow: true,
+		listeners: {
+		    destroy: () => me.load(),
+		},
+	    });
+	},
+
+	addHost: function() {
+	    let me = this;
+	    me.edit(false);
+	},
+
+	edit: function(includeNodename = true) {
+	    let me = this;
+	    let view = me.getView();
+	    let selection = view.getSelection();
+	    if (!selection || !selection.length) {
+		return;
+	    }
+	    let rec = selection[0];
+	    if (!view.canConfigure) {
+		return;
+	    }
+
+	    let type = 'PVE.node.' + (rec.data.type === 'pci' ? 'PCIEditWindow' : 'USBEditWindow');
+
+	    Ext.create(type, {
+		url: `/nodes/${rec.data.node}/hardware/mapping/${rec.data.type}/${rec.data.entry}`,
+		autoShow: true,
+		autoLoad: rec.data.ntype !== 'entry',
+		nodename: rec.data.ntype !== 'entry' && includeNodename ? rec.data.node : undefined,
+		name: rec.data.entry ?? rec.data.text,
+		listeners: {
+		    destroy: () => me.load(),
+		},
+	    });
+	},
+
+	load: function() {
+	    let me = this;
+	    let view = me.getView();
+	    Proxmox.Utils.API2Request({
+		url: '/cluster/hardware/mapping',
+		method: 'GET',
+		failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+		success: function({ result: { data } }) {
+		    view.setRootNode({
+			children: data,
+		    });
+		    let root = view.getRootNode();
+		    root.expand();
+		    root.childNodes.forEach(node => node.expand());
+		    me.loadRemainigNodes();
+		},
+	    });
+	},
+
+	loadRemainigNodes: function() {
+	    let me = this;
+	    let view = me.getView();
+	    PVE.data.ResourceStore.getNodes().forEach(({ node }) => {
+		if (node === Proxmox.NodeName) {
+		    return;
+		}
+		Proxmox.Utils.API2Request({
+		    url: `/nodes/${node}/hardware/mapping/all`,
+		    method: 'GET',
+		    failure: function(response) {
+			view.getRootNode()?.cascade(function(rec) {
+			    if (rec.data.node !== node) {
+				return;
+			    }
+
+			    rec.set('valid', 0);
+			    rec.set('errmsg', response.htmlStatus);
+			    rec.commit();
+			});
+		    },
+		    success: function({ result: { data } }) {
+			let entries = {};
+			data.forEach((entry) => {
+			    entries[entry.name] = entry;
+			});
+			view.getRootNode()?.cascade(function(rec) {
+			    if (rec.data.node !== node) {
+				return;
+			    }
+
+			    let entry = entries[rec.data.entry];
+
+			    rec.set('valid', entry.valid);
+			    rec.set('errmsg', entry.errmsg);
+			    rec.commit();
+			});
+		    },
+		});
+	    });
+	},
+    },
+
+    store: {
+	sorters: 'text',
+	model: 'pve-hardware-tree',
+	data: {},
+    },
+
+
+    tbar: [
+	{
+	    text: gettext('Add'),
+	    cbind: {
+		disabled: '{!canConfigure}',
+	    },
+	    menu: [
+		{
+		    text: gettext('PCI'),
+		    iconCls: 'pve-itype-icon-pci',
+		    handler: 'addPCI',
+		},
+		{
+		    text: gettext('USB'),
+		    iconCls: 'fa fa-fw fa-usb black',
+		    handler: 'addUSB',
+		},
+	    ],
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('New Host mapping'),
+	    disabled: true,
+	    parentXType: 'treepanel',
+	    enableFn: function(_rec) {
+		return this.up('treepanel').canConfigure;
+	    },
+	    handler: 'addHost',
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Edit'),
+	    disabled: true,
+	    parentXType: 'treepanel',
+	    enableFn: function(rec) {
+		return rec.data.ntype !== 'entry' && this.up('treepanel').canConfigure;
+	    },
+	    cbind: {
+		disabled: '{!canConfigure}',
+	    },
+	    handler: 'edit',
+	},
+	{
+	    xtype: 'proxmoxStdRemoveButton',
+	    parentXType: 'treepanel',
+	    getUrl: function(rec) {
+		let data = rec.data;
+		return `/api2/extjs/nodes/${data.node}/hardware/mapping/${data.type}/${data.entry}`;
+	    },
+	    confirmMsg: function(rec) {
+		let msg = gettext('Are you sure you want to remove entry {0} for {1}');
+		return Ext.String.format(msg, `'${rec.data.entry}'`, `'${rec.data.node}'`);
+	    },
+	    enableFn: function(rec) {
+		return rec.data.ntype !== 'entry' && this.up('treepanel').canConfigure;
+	    },
+	    callback: 'load',
+	    disabled: true,
+	    text: gettext('Remove'),
+	},
+    ],
+
+    columns: [
+	{
+	    xtype: 'treecolumn',
+	    text: gettext('Type/ID/Node'),
+	    dataIndex: 'text',
+	    renderer: function(value, _meta, record) {
+		if (record.data.ntype === 'entry') {
+		    let typeMap = {
+			usb: gettext('USB'),
+			pci: gettext('PCI'),
+		    };
+		    let type = typeMap[record.data.type] || Proxmox.Utils.unknownText;
+		    return `${value} (${type})`;
+		}
+		return value;
+	    },
+	    width: 200,
+	},
+	{
+	    text: gettext('Vendor'),
+	    dataIndex: 'vendor',
+	},
+	{
+	    text: gettext('Device'),
+	    dataIndex: 'device',
+	},
+	{
+	    text: gettext('Subsystem Vendor'),
+	    dataIndex: 'subsystem-vendor',
+	},
+	{
+	    text: gettext('Subsystem Device'),
+	    dataIndex: 'subsystem-device',
+	},
+	{
+	    text: gettext('IOMMU group'),
+	    dataIndex: 'iommugroup',
+	},
+	{
+	    text: gettext('Path'),
+	    flex: 1,
+	    dataIndex: 'path',
+	    renderer: function(value) {
+		value = value ?? '';
+		if (value.indexOf(';') !== -1) {
+		    return value.split(';').join(', ');
+		}
+		return value;
+	    },
+	},
+	{
+	    header: gettext('Status'),
+	    dataIndex: 'valid',
+	    flex: 1,
+	    renderer: function(value, _metadata, record) {
+		if (record.data.ntype !== 'mapping') {
+		    return '';
+		}
+		let iconCls;
+		let status;
+		if (value === undefined) {
+		    iconCls = 'fa-spinner fa-spin';
+		    status = gettext('Loading...');
+		} else {
+		    let state = value ? 'good' : 'critical';
+		    iconCls = PVE.Utils.get_health_icon(state, true);
+		    status = value ? gettext("OK") : record.data.errmsg || Proxmox.Utils.unknownText;
+		}
+		return `<i class="fa ${iconCls}"></i> ${status}`;
+	    },
+	},
+	{
+	    header: gettext('Comment'),
+	    dataIndex: 'comment',
+	    flex: 1,
+	},
+    ],
+
+    listeners: {
+	activate: 'load',
+	itemdblclick: 'edit',
+    },
+});
-- 
2.30.2






More information about the pve-devel mailing list