From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH manager v3 1/1] ui: implement 'Tag View' for the resource tree
Date: Thu, 7 Nov 2024 12:25:21 +0100 [thread overview]
Message-ID: <20241107112521.2075598-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20241107112521.2075598-1-d.csapak@proxmox.com>
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 <d.csapak@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/Workspace.js | 4 +-
www/manager6/form/ViewSelector.js | 28 +++++++++++
www/manager6/grid/ResourceGrid.js | 2 +-
www/manager6/panel/TagConfig.js | 6 +++
www/manager6/tree/ResourceTree.js | 81 ++++++++++++++++++++++++++++---
6 files changed, 113 insertions(+), 9 deletions(-)
create mode 100644 www/manager6/panel/TagConfig.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 2c3a822b..bcf44c39 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -108,6 +108,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 52c66108..ca29b3f8 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({
@@ -531,7 +532,8 @@ Ext.define('PVE.StdWorkspace', {
let tagSelectors = [];
['circle', 'dense'].forEach((style) => {
['dark', 'light'].forEach((variant) => {
- tagSelectors.push(`.proxmox-tags-${style} .proxmox-tag-${variant}`);
+ let selector = `.proxmox-tags-${style} :not(.proxmox-tags-full) > .proxmox-tag-${variant}`;
+ tagSelectors.push(selector);
});
});
diff --git a/www/manager6/form/ViewSelector.js b/www/manager6/form/ViewSelector.js
index e25547c4..ee16c227 100644
--- a/www/manager6/form/ViewSelector.js
+++ b/www/manager6/form/ViewSelector.js
@@ -32,6 +32,34 @@ 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 `<span class="proxmox-tags-full">${tag}</span>`;
+ },
+ 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 d3c15aec..7b0c5dc8 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',
+ },
},
},
@@ -165,12 +168,15 @@ Ext.define('PVE.tree.ResourceTree', {
me.setText(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);
@@ -267,6 +273,30 @@ Ext.define('PVE.tree.ResourceTree', {
'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags',
];
+ // special case ids from the tag view, since they change the id in the state
+ let idMapFn = function(id) {
+ if (!id) {
+ return undefined;
+ }
+ if (id.startsWith('qemu') || id.startsWith('lxc')) {
+ let [realId, _tag] = id.split('-');
+ return realId;
+ }
+ return id;
+ };
+
+ let findNode = function(rootNode, id) {
+ if (!id) {
+ return undefined;
+ }
+ let node = rootNode.findChild('id', id, true);
+ if (!node) {
+ node = rootNode.findChildBy((r) => idMapFn(r.data.id) === idMapFn(id), undefined, true);
+ }
+ return node;
+ };
+
+
let updateTree = function() {
store.suspendEvents();
@@ -282,6 +312,8 @@ 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 ?? {};
+
// also check for name for when the tree is sorted by name
let moveCheckAttrs = groups.concat(['node', 'template', 'name']);
let filterfn = me.viewFilter.filterfn;
@@ -291,13 +323,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;
}
@@ -317,6 +356,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);
olditem.commit();
@@ -333,10 +375,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;
@@ -355,8 +402,10 @@ Ext.define('PVE.tree.ResourceTree', {
store.resumeEvents();
store.fireEvent('refresh', store);
+ let foundChild = findNode(rootnode, lastsel?.data.id);
+
// select parent node if original selected node vanished
- if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) {
+ if (lastsel && !foundChild) {
lastsel = rootnode;
for (const node of parents) {
if (rootnode.findChild('id', node.data.id, true)) {
@@ -476,7 +525,7 @@ Ext.define('PVE.tree.ResourceTree', {
if (nodeid === 'root') {
node = rootnode;
} else {
- node = rootnode.findChild('id', nodeid, true);
+ node = findNode(rootnode, nodeid);
}
if (node) {
me.selectExpand(node);
@@ -498,6 +547,24 @@ Ext.define('PVE.tree.ResourceTree', {
rstore.on("load", updateTree);
rstore.startUpdate();
+
+ 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);
+ if (me.viewFilter.groupRenderer) {
+ info.text = me.viewFilter.groupRenderer(info);
+ }
+ node.commit();
+ }
+ return true;
+ },
+ });
+ });
},
});
--
2.39.5
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2024-11-07 11:25 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2024-11-07 11:25 [pve-devel] [PATCH docs/widget-toolkit/manager v3] implement tagview Dominik Csapak
2024-11-07 11:25 ` [pve-devel] [PATCH docs v3 1/1] gui: add anchor for tags chapter Dominik Csapak
2024-11-07 11:25 ` [pve-devel] [PATCH widget-toolkit v3 1/1] css: add some conditions to the tag classes for the tag view Dominik Csapak
2024-11-07 11:25 ` Dominik Csapak [this message]
2024-11-07 13:52 ` [pve-devel] [PATCH docs/widget-toolkit/manager v3] implement tagview Aaron Lauterer
2024-11-07 14:16 ` Dominik Csapak
2024-11-07 14:20 ` Aaron Lauterer
2024-11-08 9:44 ` Daniel Herzig
2024-11-10 11:22 ` [pve-devel] applied-series: " Thomas Lamprecht
2024-11-11 13:58 ` Dominik Csapak
2024-11-11 14:26 ` Thomas Lamprecht
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=20241107112521.2075598-4-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