public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v5 12/15] ui: add ResourceMapTree
Date: Tue,  6 Jun 2023 15:52:19 +0200	[thread overview]
Message-ID: <20230606135222.984747-21-d.csapak@proxmox.com> (raw)
In-Reply-To: <20230606135222.984747-1-d.csapak@proxmox.com>

this will be the base class for trees for the individual mapping types,
e.g. pci and usb mapping.

there are a few things to configure, but the overall code sharing is
still significant, and should work out fine for future mapping types

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/Makefile                |   1 +
 www/manager6/tree/ResourceMapTree.js | 316 +++++++++++++++++++++++++++
 2 files changed, 317 insertions(+)
 create mode 100644 www/manager6/tree/ResourceMapTree.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index c94aee64..938ec9f1 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -101,6 +101,7 @@ JSSRC= 							\
 	panel/MultiDiskEdit.js				\
 	tree/ResourceTree.js				\
 	tree/SnapshotTree.js				\
+	tree/ResourceMapTree.js				\
 	window/Backup.js				\
 	window/BackupConfig.js				\
 	window/BulkAction.js				\
diff --git a/www/manager6/tree/ResourceMapTree.js b/www/manager6/tree/ResourceMapTree.js
new file mode 100644
index 00000000..df50b63a
--- /dev/null
+++ b/www/manager6/tree/ResourceMapTree.js
@@ -0,0 +1,316 @@
+Ext.define('PVE.tree.ResourceMapTree', {
+    extend: 'Ext.tree.Panel',
+    alias: 'widget.pveResourceMapTree',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    rootVisible: false,
+
+    emptyText: gettext('No Mapping found'),
+
+    // will be opened on edit
+    editWindowClass: undefined,
+
+    // The base url of the resource
+    baseUrl: undefined,
+
+    // icon class to show on the entries
+    mapIconCls: undefined,
+
+    // if given, should be a function that takes a nodename and returns
+    // the url for getting the data to check the status
+    getStatusCheckUrl: undefined,
+
+    // the result of above api call and the nodename is passed and can set the status
+    checkValidity: undefined,
+
+    // the property that denotes a single map entry for a node
+    entryIdProperty: undefined,
+
+    cbindData: function(initialConfig) {
+	let me = this;
+	const caps = Ext.state.Manager.get('GuiCap');
+	me.canConfigure = !!caps.mapping['Mapping.Modify'];
+
+	return {};
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	addMapping: function() {
+	    let me = this;
+	    let view = me.getView();
+	    Ext.create(view.editWindowClass, {
+		url: view.baseUrl,
+		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 || (rec.data.type === 'entry' && includeNodename)) {
+		return;
+	    }
+
+	    Ext.create(view.editWindowClass, {
+		url: `${view.baseUrl}/${rec.data.name}`,
+		autoShow: true,
+		autoLoad: true,
+		nodename: includeNodename ? rec.data.node : undefined,
+		name: rec.data.name,
+		listeners: {
+		    destroy: () => me.load(),
+		},
+	    });
+	},
+
+	remove: function() {
+	    let me = this;
+	    let view = me.getView();
+	    let selection = view.getSelection();
+	    if (!selection || !selection.length) {
+		return;
+	    }
+
+	    let data = selection[0].data;
+	    let url = `${view.baseUrl}/${data.name}`;
+	    let method = 'PUT';
+	    let params = {
+		digest: me.lookup[data.name].digest,
+	    };
+	    let map = me.lookup[data.name].map;
+	    switch (data.type) {
+		case 'entry':
+		    method = 'DELETE';
+		    params = undefined;
+		    break;
+		case 'node':
+		    params.map = PVE.Parser.filterPropertyStringList(map, (e) => e.node !== data.node);
+		    break;
+		case 'map':
+		    params.map = PVE.Parser.filterPropertyStringList(map, (e) =>
+			Object.entries(e).some(([key, value]) => data[key] !== value));
+		    break;
+		default:
+		    throw "invalid type";
+	    }
+	    if (!params?.map.length) {
+		method = 'DELETE';
+		params = undefined;
+	    }
+	    Proxmox.Utils.API2Request({
+		url,
+		method,
+		params,
+		success: function() {
+		    me.load();
+		},
+	    });
+	},
+
+	load: function() {
+	    let me = this;
+	    let view = me.getView();
+	    Proxmox.Utils.API2Request({
+		url: view.baseUrl,
+		method: 'GET',
+		failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+		success: function({ result: { data } }) {
+		    let lookup = {};
+		    data.forEach((entry) => {
+			lookup[entry.id] = Ext.apply({}, entry);
+			entry.iconCls = 'fa fa-fw fa-folder-o';
+			entry.name = entry.id;
+			entry.text = entry.id;
+			entry.type = 'entry';
+
+			let nodes = {};
+			for (const map of entry.map) {
+			    let parsed = PVE.Parser.parsePropertyString(map);
+			    parsed.iconCls = view.mapIconCls;
+			    parsed.leaf = true;
+			    parsed.name = entry.id;
+			    parsed.text = parsed[view.entryIdProperty];
+			    parsed.type = 'map';
+
+			    if (nodes[parsed.node] === undefined) {
+				nodes[parsed.node] = {
+				    children: [],
+				    expanded: true,
+				    iconCls: 'fa fa-fw fa-building-o',
+				    leaf: false,
+				    name: entry.id,
+				    node: parsed.node,
+				    text: parsed.node,
+				    type: 'node',
+				};
+			    }
+			    nodes[parsed.node].children.push(parsed);
+			}
+			delete entry.id;
+			entry.children = Object.values(nodes);
+			entry.leaf = entry.children.length === 0;
+		    });
+		    me.lookup = lookup;
+		    if (view.getStatusCheckUrl !== undefined && view.checkValidity !== undefined) {
+			me.loadStatusData();
+		    }
+		    view.setRootNode({
+			children: data,
+		    });
+		    let root = view.getRootNode();
+		    root.expand();
+		    root.childNodes.forEach(node => node.expand());
+		},
+	    });
+	},
+
+	nodeLoadingState: {},
+
+	loadStatusData: function() {
+	    let me = this;
+	    let view = me.getView();
+	    PVE.data.ResourceStore.getNodes().forEach(({ node }) => {
+		me.nodeLoadingState[node] = true;
+		let url = view.getStatusCheckUrl(node);
+		Proxmox.Utils.API2Request({
+		    url,
+		    method: 'GET',
+		    failure: function(response) {
+			me.nodeLoadingState[node] = false;
+			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 } }) {
+			me.nodeLoadingState[node] = false;
+			view.checkValidity(data, node);
+		    },
+		});
+	    });
+	},
+
+	renderStatus: function(value, _metadata, record) {
+	    let me = this;
+	    if (record.data.type !== 'map') {
+		return '';
+	    }
+	    let iconCls;
+	    let status;
+	    if (value === undefined) {
+		if (me.nodeLoadingState[record.data.node]) {
+		    iconCls = 'fa-spinner fa-spin';
+		    status = gettext('Loading...');
+		} else {
+		    iconCls = 'fa-question-circle';
+		    status = gettext('Unknown Node');
+		}
+	    } 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}`;
+	},
+
+	init: function(view) {
+	    let me = this;
+
+	    ['editWindowClass', 'baseUrl', 'mapIconCls', 'entryIdProperty'].forEach((property) => {
+		if (view[property] === undefined) {
+		    throw `No ${property} defined`;
+		}
+	    });
+
+	    me.load();
+	},
+    },
+
+    store: {
+	sorters: 'text',
+	data: {},
+    },
+
+
+    tbar: [
+	{
+	    text: gettext('Add mapping'),
+	    handler: 'addMapping',
+	    cbind: {
+		disabled: '{!canConfigure}',
+	    },
+	},
+	{
+	    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 && rec.data.type !== 'entry' && this.up('treepanel').canConfigure;
+	    },
+	    handler: 'edit',
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    parentXType: 'treepanel',
+	    handler: 'remove',
+	    disabled: true,
+	    text: gettext('Remove'),
+	    enableFn: function(rec) {
+		return rec && this.up('treepanel').canConfigure;
+	    },
+	    confirmMsg: function(rec) {
+		let msg, id;
+		let view = this.up('treepanel');
+		switch (rec.data.type) {
+		    case 'entry':
+			msg = gettext("Are you sure you want to remove '{0}'");
+			return Ext.String.format(msg, rec.data.name);
+		    case 'node':
+			msg = gettext("Are you sure you want to remove '{0}' entries for '{1}'");
+			return Ext.String.format(msg, rec.data.node, rec.data.name);
+		    case 'map':
+			msg = gettext("Are you sure you want to remove '{0}' on '{1}' for '{2}'");
+			id = rec.data[view.entryIdProperty];
+			return Ext.String.format(msg, id, rec.data.node, rec.data.name);
+		    default:
+			throw "invalid type";
+		}
+	    },
+	},
+    ],
+
+    listeners: {
+	itemdblclick: 'edit',
+    },
+});
-- 
2.30.2





  parent reply	other threads:[~2023-06-06 13:53 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-06-06 13:51 [pve-devel] [PATCH access-control/guest-common/qemu-server/manager v5] cluster mapping Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH access-control v5 1/1] add privileges and paths for cluster resource mapping Dominik Csapak
2023-06-07 17:03   ` [pve-devel] applied: " Thomas Lamprecht
2023-06-06 13:52 ` [pve-devel] [PATCH guest-common v5 1/1] add PCI/USB Mapping configs Dominik Csapak
2023-06-07 17:17   ` [pve-devel] applied: " Thomas Lamprecht
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 1/6] enable cluster mapped USB devices for guests Dominik Csapak
2023-06-13 12:23   ` Wolfgang Bumiller
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 2/6] enable cluster mapped PCI " Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 3/6] check_local_resources: extend for mapped resources Dominik Csapak
2023-06-13 12:43   ` Wolfgang Bumiller
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 4/6] api: migrate preconditions: use new check_local_resources info Dominik Csapak
2023-06-13 12:46   ` Wolfgang Bumiller
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 5/6] migration: check for mapped resources Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 6/6] add test for mapped pci devices Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 01/15] pvesh: fix parameters for proxyto_callback Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 02/15] api: add resource map api endpoints for PCI and USB Dominik Csapak
2023-06-13 11:26   ` Wolfgang Bumiller
2023-06-06 13:52 ` [pve-devel] [PATCH v5 03/15] ui: parser: add helpers for lists of property strings Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 04/15] ui: form/USBSelector: make it more flexible with nodename Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 05/15] ui: form: add PCIMapSelector Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 06/15] ui: form: add USBMapSelector Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 07/15] ui: qemu/PCIEdit: rework panel to add a mapped configuration Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 08/15] ui: qemu/USBEdit: add 'mapped' device case Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 09/15] ui: form: add MultiPCISelector Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 10/15] ui: add edit window for pci mappings Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 11/15] ui: add edit window for usb mappings Dominik Csapak
2023-06-06 13:52 ` Dominik Csapak [this message]
2023-06-06 13:52 ` [pve-devel] [PATCH v5 13/15] ui: allow configuring pci and usb mapping Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 14/15] ui: window/Migrate: allow mapped devices Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 15/15] ui: improve permission handling for hardware Dominik Csapak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20230606135222.984747-21-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal