From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 93D08A20BB for ; Fri, 16 Jun 2023 15:06:22 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 59D86334DA for ; Fri, 16 Jun 2023 15:05:52 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Fri, 16 Jun 2023 15:05:49 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id CE21745B6A for ; Fri, 16 Jun 2023 15:05:48 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Fri, 16 Jun 2023 15:05:38 +0200 Message-Id: <20230616130542.199182-19-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230616130542.199182-1-d.csapak@proxmox.com> References: <20230616130542.199182-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -1.485 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - URIBL_BLACK 3 Contains an URL listed in the URIBL blacklist [entry.name] Subject: [pve-devel] [PATCH manager v7 11/14] ui: add ResourceMapTree X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 16 Jun 2023 13:06:22 -0000 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 --- 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 99ebc4dc..0cb922d6 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 ` ${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