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 7B994930E for ; Wed, 2 Aug 2023 12:53:43 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5A38216F44 for ; Wed, 2 Aug 2023 12:53:43 +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 ; Wed, 2 Aug 2023 12:53:41 +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 0400B40802 for ; Wed, 2 Aug 2023 12:53:41 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Wed, 2 Aug 2023 12:53:38 +0200 Message-Id: <20230802105338.2428817-4-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20230802105338.2428817-1-d.csapak@proxmox.com> References: <20230802105338.2428817-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.016 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_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [item.data, data.id, info.id, olditem.data] Subject: [pve-devel] [PATCH manager 1/1] ui: implement 'Tag View' for the resource tree 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: Wed, 02 Aug 2023 10:53:43 -0000 and keep the functionality in ResourceTree as generic as possible. We achieve this by having an 'itemMap' function that can split one item from the store into multiple to add to the tree. for the updates, we have to have an 'idMapFn' (to get the original id back) we also have to modify how the move checks work a bit, since we only want to move the items when the tags changed only in the tagview case in the ResourceGrid we have to get the id a bit differently since we now have 'virtual' ids for the entries tag contain the tag (which can't be found in the resource store) Signed-off-by: Dominik Csapak --- this depends on the docs (onlineHelp ref) and widget toolkit (css) patches there is probably some optimization potential in how we handle the overrides, but i tried to keep it as generic as possible so that we can extend it easily if we want to. www/manager6/Makefile | 1 + www/manager6/Workspace.js | 1 + www/manager6/form/ViewSelector.js | 32 +++++++++++++++++++ www/manager6/grid/ResourceGrid.js | 2 +- www/manager6/panel/TagConfig.js | 6 ++++ www/manager6/tree/ResourceTree.js | 53 ++++++++++++++++++++++++++++--- 6 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 www/manager6/panel/TagConfig.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 7ec9d7a5..fdb38905 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -102,6 +102,7 @@ JSSRC= \ panel/GuestSummary.js \ panel/TemplateStatusView.js \ panel/MultiDiskEdit.js \ + panel/TagConfig.js \ tree/ResourceTree.js \ tree/SnapshotTree.js \ tree/ResourceMapTree.js \ diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js index 18d574b7..a46fa75d 100644 --- a/www/manager6/Workspace.js +++ b/www/manager6/Workspace.js @@ -247,6 +247,7 @@ Ext.define('PVE.StdWorkspace', { storage: 'PVE.storage.Browser', sdn: 'PVE.sdn.Browser', pool: 'pvePoolConfig', + tag: 'pveTagConfig', }; PVE.curSelectedNode = treeNode; me.setContent({ diff --git a/www/manager6/form/ViewSelector.js b/www/manager6/form/ViewSelector.js index e25547c4..647399ef 100644 --- a/www/manager6/form/ViewSelector.js +++ b/www/manager6/form/ViewSelector.js @@ -32,6 +32,38 @@ Ext.define('PVE.form.ViewSelector', { // Pool View only lists VMs and Containers filterfn: ({ data }) => data.type === 'qemu' || data.type === 'lxc' || data.type === 'pool', }, + tags: { + text: gettext('Tag View'), + groups: ['tag'], + filterfn: ({ data }) => data.type === 'qemu' || data.type === 'lxc', + groupRenderer: function(info) { + let tag = PVE.Utils.renderTags(info.tag, PVE.UIOptions.tagOverrides); + return `${tag}`; + }, + idMapFn: function(id) { + let [realId, _tag] = id.split('-'); + return realId; + }, + itemMap: function(item) { + let tags = (item.data.tags ?? '').split(/[;, ]/); + if (tags.length === 1 && tags[0] === '') { + return item; + } + let items = []; + for (const tag of tags) { + let id = `${item.data.id}-${tag}`; + let info = Ext.apply({ leaf: true }, item.data); + info.tag = tag; + info.realId = info.id; + info.id = id; + items.push(Ext.create('Ext.data.TreeModel', info)); + } + return items; + }, + attrMoveChecks: { + tag: (newitem, olditem) => newitem.data.tags !== olditem.data.tags, + }, + }, }; let groupdef = Object.entries(default_views).map(([name, config]) => [name, config.text]); diff --git a/www/manager6/grid/ResourceGrid.js b/www/manager6/grid/ResourceGrid.js index 9376bcc2..b212e9e9 100644 --- a/www/manager6/grid/ResourceGrid.js +++ b/www/manager6/grid/ResourceGrid.js @@ -44,7 +44,7 @@ Ext.define('PVE.grid.ResourceGrid', { return; } for (let child of node.childNodes) { - let orgNode = rstore.data.get(child.data.id); + let orgNode = rstore.data.get(child.data.realId ?? child.data.id); if (orgNode) { if ((!filterfn || filterfn(child)) && (!textfilter || textfilterMatch(child))) { nodeidx[child.data.id] = orgNode; diff --git a/www/manager6/panel/TagConfig.js b/www/manager6/panel/TagConfig.js new file mode 100644 index 00000000..203c47c2 --- /dev/null +++ b/www/manager6/panel/TagConfig.js @@ -0,0 +1,6 @@ +Ext.define('PVE.panel.TagConfig', { + extend: 'PVE.panel.Config', + alias: 'widget.pveTagConfig', + + onlineHelp: 'gui_tags', +}); diff --git a/www/manager6/tree/ResourceTree.js b/www/manager6/tree/ResourceTree.js index 54c6403d..b88e9027 100644 --- a/www/manager6/tree/ResourceTree.js +++ b/www/manager6/tree/ResourceTree.js @@ -37,6 +37,9 @@ Ext.define('PVE.tree.ResourceTree', { template: { iconCls: 'fa fa-file-o', }, + tag: { + iconCls: 'fa fa-tag', + }, }, }, @@ -161,12 +164,15 @@ Ext.define('PVE.tree.ResourceTree', { me.setToolTip(info); if (info.groupbyid) { - info.text = info.groupbyid; - if (info.type === 'type') { + if (me.viewFilter.groupRenderer) { + info.text = me.viewFilter.groupRenderer(info); + } else if (info.type === 'type') { let defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid]; if (defaults && defaults.text) { info.text = defaults.text; } + } else { + info.text = info.groupbyid; } } let child = Ext.create('PVETree', info); @@ -278,6 +284,9 @@ Ext.define('PVE.tree.ResourceTree', { let groups = me.viewFilter.groups || []; // explicitly check for node/template, as those are not always grouping attributes + let attrMoveChecks = me.viewFilter.attrMoveChecks ?? {}; + let idMapFn = me.viewFilter.idMapFn ?? Ext.identityFn; + // also check for name for when the tree is sorted by name let moveCheckAttrs = groups.concat(['node', 'template', 'name']); let filterfn = me.viewFilter.filterfn; @@ -287,13 +296,20 @@ Ext.define('PVE.tree.ResourceTree', { // remove vanished or moved items and update changed items in-place for (const [key, olditem] of Object.entries(index)) { // getById() use find(), which is slow (ExtJS4 DP5) - let item = rstore.data.get(olditem.data.id); + let oldid = olditem.data.id; + let id = idMapFn(olditem.data.id); + let item = rstore.data.get(id); let changed = sorting_changed, moved = sorting_changed; if (item) { // test if any grouping attributes changed, catches migrated tree-nodes in server view too for (const attr of moveCheckAttrs) { - if (item.data[attr] !== olditem.data[attr]) { + if (attrMoveChecks[attr]) { + if (attrMoveChecks[attr](olditem, item)) { + moved = true; + break; + } + } else if (item.data[attr] !== olditem.data[attr]) { moved = true; break; } @@ -313,6 +329,9 @@ Ext.define('PVE.tree.ResourceTree', { olditem.beginEdit(); let info = olditem.data; Ext.apply(info, item.data); + if (info.id !== oldid) { + info.id = oldid; + } me.setIconCls(info); me.setText(info); me.setToolTip(info); @@ -330,10 +349,15 @@ Ext.define('PVE.tree.ResourceTree', { // store events are suspended, so remove the item manually store.remove(olditem); parentNode.removeChild(olditem, true); + if (parentNode.childNodes.length < 1 && parentNode.parentNode) { + let grandParent = parentNode.parentNode; + grandParent.removeChild(parentNode, true); + } } } - rstore.each(function(item) { // add new items + let items = rstore.getData().items.flatMap(me.viewFilter.itemMap ?? Ext.identityFn); + items.forEach(function(item) { // add new items let olditem = index[item.data.id]; if (olditem) { return; @@ -469,6 +493,25 @@ Ext.define('PVE.tree.ResourceTree', { rstore.on("load", updateTree); rstore.startUpdate(); + + if (me.viewFilter.groupRenderer) { + me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => { + me.store.getRootNode().cascadeBy({ + before: function(node) { + if (node.data.groupbyid) { + node.beginEdit(); + let info = node.data; + me.setIconCls(info); + me.setText(info); + me.setToolTip(info); + info.text = me.viewFilter.groupRenderer(info); + node.commit(); + } + return true; + }, + }); + }); + } }, }); -- 2.30.2