public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui
@ 2022-03-23 10:34 Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH cluster v4 1/1] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
                   ` (11 more replies)
  0 siblings, 12 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

this series is a continuation of my previous attempt in 2019.
note that this requires my previous patch for pve-cluster that
introduces the 'get properties' pmxcfs message[0]

this is a bit "rough" on various points, but i wanted to send what i have
before i code myself into a hole i cannot easily get out of^^, namely:
* having gui config options in datacenter.cfg
  (gui console is a precedence here, but maybe there is still a better place?)
  also if /version is the right place to get it or if we should make another
  dedicated api call for such things
* the datacenter tags=color regex (could refactor the tags regex out in
  pve-common)
* the color editing grid in the gui:
  this is very basic but usable. on short notice i did not really find
  a better way to have that mapping from arbitrarily tags (but
  selectable from existing ones) to color
* there is no drag&drop (like @thomas wished the last time), but with
  the new inline editing it's maybe not necessary?
* no gui for the browser-settings yet:
  since there is not much space left in the 'my settings' window, i'd
  have to reorganize it, but did not come around to that yet, this can
  ofc also be done at a later stage if we ignore browser set color
  overrides for now
* the tree styling:
  not sure if it makes sense at all to have multiple styles here, though
  i always find such options nice. the 'dense' option i am a bit
  conflicted myself, it provides a better experience when having many
  tags, but you lose some contrast depending on the colors
* probably many more...

changes from v3:
* show the tags in the tree (with multiple styles)
* they are now inline editable instead of having a pop up with the editor
* able to override colors in datacenter cfg
* show a dropdown on editing with existing tags (from tree+overrides)
* show the tags in the global search grid (and make them searchable)

changes from v2:
* rebase on master (drop applied patch, merge with lxc pending changes)
* move utilities to widget-toolkit
* prefix css classes
* remove tags from options and add edit button to the tags directly
* show 'no tags' when no tags are defined
* improve statusTxt style

changes from v1:
* slightly different format (use [a-z...] instead of \w)
* add comment in JSONSchema
* better commit message
* add the tags to the status api call of guests (for gui)
* show the tags in the gui
* make the tags editable in the gui

0: https://lists.proxmox.com/pipermail/pve-devel/2022-March/052174.html

pve-cluster:

Dominik Csapak (1):
  datacenter.cfg: add option for tag-tree-style and tag-colors

 data/PVE/DataCenterConfig.pm | 13 +++++++++++++
 1 file changed, 13 insertions(+)

proxmox-widget-toolkit:

Dominik Csapak (1):
  add tag related helpers

 src/Utils.js         | 35 +++++++++++++++++++++++++++++
 src/css/ext6-pmx.css | 52 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 87 insertions(+)

pve-manager:

Dominik Csapak (10):
  api: /cluster/resources: add tags to returned properties
  api: /version: add 'tag-colors' and 'tag-tree-style'
  ui: parse and save tag color overrides from /version
  ui: tree/ResourceTree: collect tags on update
  ui: add form/TagColorGrid
  ui: dc/OptionView: add editors for tag settings
  ui: add form/Tag
  ui: {lxc,qemu}/Config: show Tags and make them editable
  ui: tree/ResourceTree: show Tags in tree
  ui: form/GlobalSearchField: display tags and allow to search for them

 PVE/API2.pm                            |  12 +-
 PVE/API2/Cluster.pm                    |   9 +-
 www/manager6/Makefile                  |   2 +
 www/manager6/Utils.js                  |  62 +++++++
 www/manager6/Workspace.js              |  13 ++
 www/manager6/data/ResourceStore.js     |   6 +
 www/manager6/dc/OptionView.js          |  43 ++++-
 www/manager6/form/GlobalSearchField.js |  19 ++-
 www/manager6/form/Tag.js               | 207 +++++++++++++++++++++++
 www/manager6/form/TagColorGrid.js      | 218 +++++++++++++++++++++++++
 www/manager6/lxc/Config.js             |  72 +++++++-
 www/manager6/qemu/Config.js            |  70 +++++++-
 www/manager6/tree/ResourceTree.js      |  20 ++-
 13 files changed, 735 insertions(+), 18 deletions(-)
 create mode 100644 www/manager6/form/Tag.js
 create mode 100644 www/manager6/form/TagColorGrid.js

-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH cluster v4 1/1] datacenter.cfg: add option for tag-tree-style and tag-colors
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH widget-toolkit v4 1/1] add tag related helpers Dominik Csapak
                   ` (10 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 data/PVE/DataCenterConfig.pm | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 6c0fa5b..e890820 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -222,6 +222,19 @@ my $datacenter_schema = {
 	    maxLength => 64 * 1024,
 	    optional => 1,
 	},
+	'tag-tree-style' => {
+	    optional => 1,
+	    type => 'string',
+	    enum => ['full', 'circle', 'dense', 'none'],
+	    default => 'circle',
+	    description => "Tag style in tree.",
+	},
+	'tag-colors' => {
+	    optional => 1,
+	    type => 'string',
+	    pattern => '(?:[a-zA-Z0-9_][a-zA-Z0-9_\-\+\.]*=[0-9a-fA-F]{6},)*(?:[a-zA-Z0-9_][a-zA-Z0-9_\-\+\.]*=[0-9a-fA-F]{6})',
+	    description => "Manual color mapping for tags. Format is '<tag>=<hex-color>'",
+	},
     },
 };
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH widget-toolkit v4 1/1] add tag related helpers
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH cluster v4 1/1] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 01/10] api: /cluster/resources: add tags to returned properties Dominik Csapak
                   ` (9 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

helpers to
* generate a color from a string consistently
* generate a html tag for a tag
* related css classes

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/Utils.js         | 35 +++++++++++++++++++++++++++++
 src/css/ext6-pmx.css | 52 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 87 insertions(+)

diff --git a/src/Utils.js b/src/Utils.js
index 6a03057..3130b2a 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1272,6 +1272,41 @@ utilities: {
 	    .map(val => val.charCodeAt(0)),
 	);
     },
+
+    stringToRGB: function(string) {
+	let hash = 0;
+	if (!string) {
+	    return hash;
+	}
+	string += 'prox'; // give short strings more variance
+	for (let i = 0; i < string.length; i++) {
+	    hash = string.charCodeAt(i) + ((hash << 5) - hash);
+	    hash = hash & hash; // to int
+	}
+	return [
+	    hash & 255,
+	    (hash >> 8) & 255,
+	    (hash >> 16) & 255,
+	    0.8, // make the colors a little lighter
+	];
+    },
+
+    rgbToCss: function(rgb) {
+	return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${rgb[3]})`;
+    },
+
+    rgbToLuminance: function(rgb) {
+	// https://en.wikipedia.org/wiki/Relative_luminance
+	return (0.2126 * rgb[0] + 0.7152*rgb[1] + 0.0722*rgb[2])/rgb[3];
+    },
+
+    getTagElement: function(string, color_overrides) {
+	let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string);
+	let bgcolor = Proxmox.Utils.rgbToCss(rgb);
+	let txtCls = Proxmox.Utils.rgbToLuminance(rgb) < 160 ? 'light' : 'dark';
+	let cls = `proxmox-tag-${txtCls}`;
+	return `<span class="${cls}" style="background-color: ${bgcolor};">${string}</span>`;
+    },
 },
 
     singleton: true,
diff --git a/src/css/ext6-pmx.css b/src/css/ext6-pmx.css
index 1d815c6..7c01d2f 100644
--- a/src/css/ext6-pmx.css
+++ b/src/css/ext6-pmx.css
@@ -6,6 +6,58 @@
     background-color: LightYellow;
 }
 
+.proxmox-tags-full .proxmox-tag-light,
+.proxmox-tags-full .proxmox-tag-dark {
+    border-radius: 9px;
+    padding: 1px 8px;
+    margin: 0px 1px;
+}
+
+.proxmox-tags-circle .proxmox-tag-light,
+.proxmox-tags-circle .proxmox-tag-dark {
+    margin: 0px 1px;
+    position: relative;
+    top: 2px;
+    border-radius: 6px;
+    height: 12px;
+    width: 12px;
+    display: inline-block;
+    color: transparent;
+    overflow: hidden;
+}
+
+.proxmox-tags-none .proxmox-tag-light,
+.proxmox-tags-none .proxmox-tag-dark {
+    display: none;
+}
+
+.proxmox-tags-dense .proxmox-tag-light,
+.proxmox-tags-dense .proxmox-tag-dark {
+    width: 5px;
+    display: inline-block;
+    color: transparent;
+    overflow: hidden;
+    vertical-align: bottom;
+}
+
+.proxmox-tags-full .proxmox-tag-light {
+    color: #fff;
+    background-color: #383838;
+}
+
+.proxmox-tags-full .proxmox-tag-light span::selection {
+    background-color: #000;
+}
+
+.proxmox-tags-full .proxmox-tag-dark {
+    color: #000;
+    background-color: #f0f0f0;
+}
+
+.proxmox-tags-full .proxmox-tag-dark span::selection {
+    background-color: #FFF;
+}
+
 .x-mask-msg-text {
     text-align: center;
 }
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 01/10] api: /cluster/resources: add tags to returned properties
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH cluster v4 1/1] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH widget-toolkit v4 1/1] add tag related helpers Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 02/10] api: /version: add 'tag-colors' and 'tag-tree-style' Dominik Csapak
                   ` (8 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

by querying 'lock' and 'tags' with 'get_guest_config_properties'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 PVE/API2/Cluster.pm | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index 227c6814..69272fc7 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -360,7 +360,8 @@ __PACKAGE__->register_method({
 
 	# we try to generate 'numbers' by using "$X + 0"
 	if (!$param->{type} || $param->{type} eq 'vm') {
-	    my $locked_vms = PVE::Cluster::get_guest_config_property('lock');
+	    my $prop_list = [qw(lock tags)];
+	    my $props = PVE::Cluster::get_guest_config_properties($prop_list);
 
 	    for my $vmid (sort keys %$idlist) {
 
@@ -392,8 +393,10 @@ __PACKAGE__->register_method({
 		# only skip now to next to ensure that the pool stats above are filled, if eligible
 		next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
 
-		if (defined(my $lock = $locked_vms->{$vmid}->{lock})) {
-		    $entry->{lock} = $lock;
+		for my $prop (@$prop_list) {
+		    if (defined(my $value = $props->{$vmid}->{$prop})) {
+			$entry->{$prop} = $value;
+		    }
 		}
 
 		if (defined($entry->{pool}) &&
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 02/10] api: /version: add 'tag-colors' and 'tag-tree-style'
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (2 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 01/10] api: /cluster/resources: add tags to returned properties Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 03/10] ui: parse and save tag color overrides from /version Dominik Csapak
                   ` (7 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

to be able to get them in the gui directly after login

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 PVE/API2.pm | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/PVE/API2.pm b/PVE/API2.pm
index a4256160..0592d4de 100644
--- a/PVE/API2.pm
+++ b/PVE/API2.pm
@@ -111,6 +111,16 @@ __PACKAGE__->register_method ({
 		optional => 1,
 		description => 'The default console viewer to use.',
 	    },
+	    'tag-colors' => {
+		type => 'string',
+		optional => 1,
+		description => 'Cluster wide tag color overrides',
+	    },
+	    'tag-tree-style' => {
+		type => 'string',
+		optional => 1,
+		description => 'Tag style in tree',
+	    },
 	},
     },
     code => sub {
@@ -119,7 +129,7 @@ __PACKAGE__->register_method ({
 	my $res = {};
 
 	my $datacenter_confg = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
-	for my $k (qw(console)) {
+	for my $k (qw(console tag-colors tag-tree-style)) {
 	    $res->{$k} = $datacenter_confg->{$k} if exists $datacenter_confg->{$k};
 	}
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 03/10] ui: parse and save tag color overrides from /version
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (3 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 02/10] api: /version: add 'tag-colors' and 'tag-tree-style' Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 04/10] ui: tree/ResourceTree: collect tags on update Dominik Csapak
                   ` (6 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

and implement a convenience function that also parses the 'color'
state from the browser storage for overrides

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/Utils.js     | 34 ++++++++++++++++++++++++++++++++++
 www/manager6/Workspace.js | 13 +++++++++++++
 2 files changed, 47 insertions(+)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index aafe359a..0ad3a482 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1803,6 +1803,40 @@ Ext.define('PVE.Utils', {
 
 	return undefined;
     },
+
+    parseTagOverrides: function(overrides) {
+	let colors = {};
+	(overrides || "").split(/[;, ]/).forEach(color => {
+	    if (!color) {
+		return;
+	    }
+	    let [tag, color_hex] = color.split('=', 2);
+	    let r = parseInt(color_hex.slice(0, 2), 16);
+	    let g = parseInt(color_hex.slice(2, 4), 16);
+	    let b = parseInt(color_hex.slice(4, 6), 16);
+	    colors[tag] = [r, g, b, 1.0];
+	});
+	return colors;
+    },
+
+    getTagOverrides: function() {
+	let sp = Ext.state.Manager.getProvider();
+	let color_state = sp.get('colors', '');
+	let browser_colors = PVE.Utils.parseTagOverrides(color_state);
+	return Ext.apply({}, browser_colors, PVE.TagColors);
+    },
+
+    updateTagSettings: function(overrides, style) {
+	if (overrides) {
+	    PVE.TagColors = PVE.Utils.parseTagOverrides(overrides);
+	}
+
+	if (style === undefined || style === '__default__') {
+	    style = 'circle';
+	}
+
+	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
+    },
 },
 
     singleton: true,
diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js
index 37d772b8..d9875c18 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -155,6 +155,7 @@ Ext.define('PVE.StdWorkspace', {
 		success: function(response) {
 		    PVE.VersionInfo = response.result.data;
 		    me.updateVersionInfo();
+		    me.updateTags();
 		},
 	    });
 
@@ -213,6 +214,18 @@ Ext.define('PVE.StdWorkspace', {
 	ui.updateLayout();
     },
 
+    updateTags: function() {
+	let me = this;
+	let colors = PVE.VersionInfo?.['tag-colors'];
+	let style = PVE.VersionInfo?.['tag-tree-style'];
+
+	PVE.Utils.updateTagSettings(colors, style);
+	if (colors) {
+	    // refresh tree once
+	    PVE.data.ResourceStore.fireEvent('load');
+	}
+    },
+
     initComponent: function() {
 	let me = this;
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 04/10] ui: tree/ResourceTree: collect tags on update
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (4 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 03/10] ui: parse and save tag color overrides from /version Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 05/10] ui: add form/TagColorGrid Dominik Csapak
                   ` (5 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

into a global list, so that we have it avaiable anywhere

add a convenience 'getTagList' function, that also gets tags from
the overrides from datacenter.cfg and browser-storage

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/Utils.js              |  8 ++++++++
 www/manager6/data/ResourceStore.js |  6 ++++++
 www/manager6/tree/ResourceTree.js  | 16 ++++++++++++++--
 3 files changed, 28 insertions(+), 2 deletions(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 0ad3a482..16a81127 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1804,6 +1804,14 @@ Ext.define('PVE.Utils', {
 	return undefined;
     },
 
+    tagList: new Set(),
+
+    getTagList: function() {
+	let overrides = PVE.Utils.getTagOverrides();
+	let override_tags = Object.keys(overrides);
+	return [...new Set([...PVE.Utils.tagList, ...override_tags])].sort();
+    },
+
     parseTagOverrides: function(overrides) {
 	let colors = {};
 	(overrides || "").split(/[;, ]/).forEach(color => {
diff --git a/www/manager6/data/ResourceStore.js b/www/manager6/data/ResourceStore.js
index c7b72306..b18f7dd8 100644
--- a/www/manager6/data/ResourceStore.js
+++ b/www/manager6/data/ResourceStore.js
@@ -293,6 +293,12 @@ Ext.define('PVE.data.ResourceStore', {
 		sortable: true,
 		width: 100,
 	    },
+	    tags: {
+		header: gettext('Tags'),
+		type: 'string',
+		hidden: true,
+		sortable: true,
+	    },
 	};
 
 	let fields = [];
diff --git a/www/manager6/tree/ResourceTree.js b/www/manager6/tree/ResourceTree.js
index be90d4f7..301f7557 100644
--- a/www/manager6/tree/ResourceTree.js
+++ b/www/manager6/tree/ResourceTree.js
@@ -226,6 +226,10 @@ Ext.define('PVE.tree.ResourceTree', {
 
 	let stateid = 'rid';
 
+	const changedFields = [
+	    'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags',
+	];
+
 	let updateTree = function() {
 	    store.suspendEvents();
 
@@ -261,7 +265,7 @@ Ext.define('PVE.tree.ResourceTree', {
 		    }
 
 		    // tree item has been updated
-		    for (const field of ['text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock']) {
+		    for (const field of changedFields) {
 			if (item.data[field] !== olditem.data[field]) {
 			    changed = true;
 			    break;
@@ -294,7 +298,14 @@ Ext.define('PVE.tree.ResourceTree', {
 		}
 	    }
 
-	    rstore.each(function(item) { // add new items
+	    let tags = new Set();
+
+	    rstore.each(function(item) { // add new items and collect tags
+		if (item.data.tags) {
+		    item.data.tags.split(/[,; ]/).filter(t => !!t).forEach((tag) => {
+			tags.add(tag);
+		    });
+		}
 		let olditem = index[item.data.id];
 		if (olditem) {
 		    return;
@@ -310,6 +321,7 @@ Ext.define('PVE.tree.ResourceTree', {
 		}
 	    });
 
+	    PVE.Utils.tagList = tags;
 	    store.resumeEvents();
 	    store.fireEvent('refresh', store);
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 05/10] ui: add form/TagColorGrid
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (5 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 04/10] ui: tree/ResourceTree: collect tags on update Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 06/10] ui: dc/OptionView: add editors for tag settings Dominik Csapak
                   ` (4 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

this provides a basic grid to edit a list of tag color overrides.
We'll use this for editing the datacenter.cfg overrides and the
browser storage overrides.

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

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index e6e01bd1..225dffba 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -73,6 +73,7 @@ JSSRC= 							\
 	form/VNCKeyboardSelector.js			\
 	form/ViewSelector.js				\
 	form/iScsiProviderSelector.js			\
+	form/TagColorGrid.js				\
 	grid/BackupView.js				\
 	grid/FirewallAliases.js				\
 	grid/FirewallOptions.js				\
diff --git a/www/manager6/form/TagColorGrid.js b/www/manager6/form/TagColorGrid.js
new file mode 100644
index 00000000..bd8e539e
--- /dev/null
+++ b/www/manager6/form/TagColorGrid.js
@@ -0,0 +1,218 @@
+Ext.define('PVE.form.ColorPicker', {
+    extend: 'Ext.Component',
+    alias: 'widget.pveColorPicker',
+
+    defaultBindProperty: 'value',
+
+    config: {
+	value: null,
+    },
+
+    height: 15,
+    maxHeight: 15,
+
+    getValue: function() {
+	return this.realvalue.slice(1);
+    },
+
+    setValue: function(value = "000000") {
+	let me = this;
+	me.picker.value = value[0] !== '#' ? `#${value}` : value;
+	me.setColor(value);
+    },
+
+    setColor: function(value = "000000") {
+	let me = this;
+	let oldValue = me.realvalue;
+	me.realvalue = value;
+	me.setStyle('background-color', `#${value}`);
+	me.fireEvent('change', me, me.realvalue, oldValue);
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.picker = document.createElement('input');
+	me.picker.type = 'color';
+	me.picker.style = `opacity: 0; border: 0px; width: 100%; height: ${me.height}px`;
+	me.picker.value = `${me.value}`;
+	me.contentEl = me.picker;
+
+	me.callParent();
+	me.picker.oninput = function() {
+	    me.setColor(me.picker.value);
+	};
+    },
+});
+
+Ext.define('PVE.form.TagColorGrid', {
+    extend: 'Ext.grid.Panel',
+    alias: 'widget.pveTagColorGrid',
+
+    mixins: [
+	'Ext.form.field.Field',
+    ],
+
+    allowBlank: true,
+    selectAll: false,
+    isFormField: true,
+    deleteEmpty: false,
+    selModel: 'checkboxmodel',
+
+    config: {
+	deleteEmpty: false,
+    },
+
+    emptyText: gettext('No Overrides'),
+    viewConfig: {
+	deferEmptyText: false,
+    },
+
+    setValue: function(value) {
+	let me = this;
+	if (!value) {
+	    me.getStore().removeAll();
+	    me.checkChange();
+	    return me;
+	}
+	let entries = (value.split(/[;, ]/) || []).map((entry) => {
+	    let [tag, color] = entry.split(/[=]/);
+	    return {
+		tag,
+		color,
+	    };
+	});
+	me.getStore().setData(entries);
+	me.checkChange();
+	return me;
+    },
+
+    getValue: function() {
+	let me = this;
+	let values = [];
+	me.getStore().each((rec) => {
+	    if (rec.data.tag) {
+		values.push(`${rec.data.tag}=${rec.data.color}`);
+	    }
+	});
+	return values.join(',');
+    },
+
+    getErrors: function(value) {
+	return [];
+    },
+
+    // override framework function to implement deleteEmpty behaviour
+    getSubmitData: function() {
+	let me = this,
+	    data = null,
+	    val;
+	if (!me.disabled && me.submitValue) {
+	    val = me.getValue();
+	    if (val !== null && val !== '') {
+		data = {};
+		data[me.getName()] = val;
+	    } else if (me.getDeleteEmpty()) {
+		data = {};
+		data.delete = me.getName();
+	    }
+	}
+	return data;
+    },
+
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	addLine: function() {
+	    let me = this;
+	    me.getView().getStore().add({});
+	},
+
+	removeSelection: function() {
+	    let me = this;
+	    let view = me.getView();
+	    let selection = view.getSelection();
+	    if (selection === undefined) {
+		return;
+	    }
+
+	    selection.forEach((sel) => {
+		view.getStore().remove(sel);
+	    });
+	    view.checkChange();
+	},
+
+	fieldChange: function(field, newValue, oldValue) {
+	    let me = this;
+	    let view = me.getView();
+	    let rec = field.getWidgetRecord();
+	    let column = field.getWidgetColumn();
+	    rec.set(column.dataIndex, newValue);
+	    view.checkChange();
+	},
+    },
+
+    tbar: [
+	{
+	    text: gettext('Add'),
+	    handler: 'addLine',
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Remove'),
+	    handler: 'removeSelection',
+	    disabled: true,
+	},
+    ],
+
+    columns: [
+	{
+	    header: 'Tag',
+	    dataIndex: 'tag',
+	    xtype: 'widgetcolumn',
+	    onWidgetAttach: function(col, widget, rec) {
+		widget.getStore().setData(PVE.Utils.getTagList().map(v => ({ tag: v })));
+	    },
+	    widget: {
+		xtype: 'combobox',
+		isFormField: false,
+		maskRe: /[a-zA-Z0-9_.-]/,
+		queryMode: 'local',
+		displayField: 'tag',
+		valueField: 'tag',
+		store: {},
+		listeners: {
+		    change: 'fieldChange',
+		},
+	    },
+	    flex: 1,
+	},
+	{
+	    header: "Color",
+	    xtype: 'widgetcolumn',
+	    flex: 1,
+	    dataIndex: 'color',
+	    widget: {
+		xtype: 'pveColorPicker',
+		isFormField: false,
+		listeners: {
+		    change: 'fieldChange',
+		},
+	    },
+	},
+    ],
+
+    store: {
+	listeners: {
+	    update: function() {
+		this.commitChanges();
+	    },
+	},
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.callParent();
+	me.initField();
+    },
+});
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 06/10] ui: dc/OptionView: add editors for tag settings
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (6 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 05/10] ui: add form/TagColorGrid Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 07/10] ui: add form/Tag Dominik Csapak
                   ` (3 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

namely for 'tag-tree-style' and 'tag-colors'.
display the tag overrides directly as they will appear as tags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/Utils.js         | 20 ++++++++++++++++
 www/manager6/dc/OptionView.js | 43 ++++++++++++++++++++++++++++++++++-
 2 files changed, 62 insertions(+), 1 deletion(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 16a81127..9097af42 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1845,6 +1845,26 @@ Ext.define('PVE.Utils', {
 
 	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
     },
+
+    tagTreeStyles: {
+	'__default__': Proxmox.Utils.defaultText,
+	'full': gettext('Full'),
+	'circle': gettext('Circle'),
+	'dense': gettext('Dense'),
+	'none': Proxmox.Utils.NoneText,
+    },
+
+    renderTags: function(tagstext) {
+	let text = '';
+	if (tagstext) {
+	    let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
+	    text += ' ';
+	    tags.forEach((tag) => {
+		text += Proxmox.Utils.getTagElement(tag, PVE.Utils.getTagOverrides());
+	    });
+	}
+	return text;
+    },
 },
 
     singleton: true,
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index 6b30ede9..677ffa90 100644
--- a/www/manager6/dc/OptionView.js
+++ b/www/manager6/dc/OptionView.js
@@ -5,6 +5,7 @@ Ext.define('PVE.dc.OptionView', {
     onlineHelp: 'datacenter_configuration_file',
 
     monStoreErrors: true,
+    userCls: 'proxmox-tags-full',
 
     add_inputpanel_row: function(name, text, opts) {
 	var me = this;
@@ -284,7 +285,43 @@ Ext.define('PVE.dc.OptionView', {
 	    minValue: 1,
 	    maxValue: 64, // arbitrary but generous limit as limits are good
 	});
-
+	me.add_combobox_row('tag-tree-style', gettext('Tag Tree Style'), {
+	    renderer: (value) => PVE.Utils.tagTreeStyles[value] ?? value,
+	    comboItems: Object.entries(PVE.Utils.tagTreeStyles),
+	    defaultValue: '__default__',
+	    deleteEmpty: true,
+	});
+	me.rows['tag-colors'] = {
+	    required: true,
+	    renderer: (value) => {
+		if (value === undefined) {
+		    return gettext('No Overrides');
+		}
+		let overrides = PVE.Utils.parseTagOverrides(value);
+		let txt = '';
+		for (const tag of Object.keys(overrides)) {
+		    txt += Proxmox.Utils.getTagElement(tag, overrides);
+		}
+		return txt;
+	    },
+	    header: gettext('Tag Colors'),
+	    editor: {
+		xtype: 'proxmoxWindowEdit',
+		width: 450,
+		bodyPadding: 0,
+		subject: gettext('Tag Colors'),
+		fieldDefaults: {
+		    labelWidth: 100,
+		},
+		url: '/api2/extjs/cluster/options',
+		items: [{
+		    name: 'tag-colors',
+		    xtype: 'pveTagColorGrid',
+		    deleteEmpty: true,
+		    height: 300,
+		}],
+	    },
+	};
 	me.selModel = Ext.create('Ext.selection.RowModel', {});
 
 	Ext.apply(me, {
@@ -319,6 +356,10 @@ Ext.define('PVE.dc.OptionView', {
 	    if (rec.data.value === '__default__') {
 		delete PVE.VersionInfo.console;
 	    }
+
+	    let colors = store.getById('tag-colors')?.data?.value;
+	    let style = store.getById('tag-tree-style')?.data?.value;
+	    PVE.Utils.updateTagSettings(colors, style);
 	});
 
 	me.on('activate', me.rstore.startUpdate);
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 07/10] ui: add form/Tag
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (7 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 06/10] ui: dc/OptionView: add editors for tag settings Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 08/10] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
                   ` (2 subsequent siblings)
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

displays a single tag, with the ability to edit inline on click. This brings
up a list of globally available tags for simple selection.

Also has a mode for adding a new Tag.

This has a 'updateCallback' which is called when the value changes
so that the parent component can update the config, and a
'layoutCallback' which will be called on input, so that the parent
component can update the layout when the content changes.
This is necessary since we circumvent the extjs logic for updating.

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

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 225dffba..45862e71 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -74,6 +74,7 @@ JSSRC= 							\
 	form/ViewSelector.js				\
 	form/iScsiProviderSelector.js			\
 	form/TagColorGrid.js				\
+	form/Tag.js					\
 	grid/BackupView.js				\
 	grid/FirewallAliases.js				\
 	grid/FirewallOptions.js				\
diff --git a/www/manager6/form/Tag.js b/www/manager6/form/Tag.js
new file mode 100644
index 00000000..5c308f51
--- /dev/null
+++ b/www/manager6/form/Tag.js
@@ -0,0 +1,207 @@
+Ext.define('Proxmox.Tag', {
+    extend: 'Ext.Component',
+    alias: 'widget.pmxTag',
+
+    // if set to true, displays 'Add Tag' and a plus symbol
+    addTag: false,
+
+    // callback to update the config
+    updateCallback: Ext.emptyFn,
+
+    // callback to update the layout in the containing element
+    // this is necessary since we circumvent extjs layout with 'contentEditable'
+    layoutCallback: Ext.emptyFn,
+
+    inEdit: false,
+    style: {
+	'padding-right': '1px',
+	'white-space': 'nowrap',
+    },
+
+    icons: {
+	addTag: 'plus',
+	normal: 'times',
+	edit: 'check',
+    },
+
+    // we need to do this in mousedown, because that triggers before
+    // focusleave (which triggers before click)
+    onMouseDown: function(event) {
+	let me = this;
+	if (event.target.tagName === "I" && (!me.addTag || me.inEdit)) {
+	    if (!me.inEdit) {
+		me.setVisible(false);
+		me.setText('');
+	    }
+	    me.finishEdit();
+	}
+    },
+
+    onClick: function(event) {
+	let me = this;
+	if ((me.addTag || event.target.tagName === "SPAN") && !me.inEdit) {
+	    if (me.addTag) {
+		me.setText('');
+		me.setStyle('cursor');
+	    }
+	    me.inEdit = true;
+	    me.tagEl().contentEditable = true;
+	    me.iconEl().classList = `fa fa-fw fa-${me.icons.edit}-circle`;
+
+	    // select text in the element
+	    let range = document.createRange();
+	    range.selectNodeContents(me.tagEl());
+	    let sel = window.getSelection();
+	    sel.removeAllRanges();
+	    sel.addRange(range);
+
+	    me.showPicker();
+	}
+    },
+
+    showPicker: function() {
+	let me = this;
+	if (!me.picker) {
+	    me.picker = Ext.widget({
+		xtype: 'boundlist',
+		minWidth: 70,
+		scrollable: true,
+		floating: true,
+		hidden: true,
+		userCls: 'proxmox-tags-circle',
+		displayField: 'tag',
+		itemTpl: [
+		    '{[Proxmox.Utils.getTagElement(values.tag, PVE.Utils.getTagOverrides())]} {tag}',
+		],
+		store: [],
+		listeners: {
+		    select: function(picker, rec) {
+			me.setText(rec.data.tag);
+			me.finishEdit();
+		    },
+		},
+	    });
+	}
+	me.picker.getStore().clearFilter();
+	let taglist = PVE.Utils.getTagList().map(v => ({ tag: v }));
+	if (taglist.length < 1) {
+	    return;
+	}
+	me.picker.getStore().setData(taglist);
+	me.picker.showBy(me, 'tl-bl');
+	me.picker.setMaxHeight(200);
+    },
+
+    finishEdit: function(update = true) {
+	let me = this;
+	me.picker?.hide();
+
+	let tag = me.tagEl().innerHTML;
+	let mode;
+	if (me.addTag) {
+	    me.setText(me.tag);
+	    me.setStyle('cursor', 'pointer');
+	    mode = 'addTag';
+	} else {
+	    me.tag = tag;
+	    mode = 'normal';
+	}
+	me.iconEl().classList = `fa fa-fw fa-${me.icons[mode]}-circle`;
+	me.updateColor(me.tag);
+
+
+	if (update) {
+	    me.updateCallback(tag);
+	}
+	me.tagEl().contentEditable = false;
+	me.inEdit = false;
+    },
+
+    setText: function(text) {
+	let me = this;
+	me.tagEl().innerHTML = text;
+	me.layoutCallback();
+    },
+
+    cancelEdit: function(list, event) {
+	let me = this;
+	if (me.inEdit) {
+	    me.setText(me.tag);
+	    me.finishEdit(false);
+	}
+    },
+
+    onKeyPress: function(event) {
+	let me = this;
+	let key = event.browserEvent.key;
+	if (key === "Enter") {
+	    me.finishEdit();
+	} else if (!key.match(/^[a-z0-9+\-_.]$/i)) {
+	    event.browserEvent.preventDefault();
+	    event.browserEvent.stopPropagation();
+	}
+    },
+
+    onInput: function() {
+	let me = this;
+	let tag = me.tagEl().innerHTML;
+	me.layoutCallback();
+	me.picker.getStore().filter({
+	    property: 'tag',
+	    value: tag,
+	});
+    },
+
+    listeners: {
+	mousedown: 'onMouseDown',
+	click: 'onClick',
+	focusleave: 'cancelEdit',
+	keypress: 'onKeyPress',
+	input: 'onInput',
+	element: 'el',
+    },
+
+    updateColor: function(tag) {
+	let me = this;
+	if (me.addTag) {
+	    me.setUserCls(`proxmox-tag-dark`);
+	    me.setStyle('background-color');
+	    return;
+	}
+	let rgb = PVE.Utils.getTagOverrides()?.[tag] ?? Proxmox.Utils.stringToRGB(tag);
+	let color = Proxmox.Utils.rgbToCss(rgb);
+	let cls = Proxmox.Utils.rgbToLuminance(rgb) < 160 ? 'light' : 'dark';
+	me.setUserCls(`proxmox-tag-${cls}`);
+	me.setStyle('background-color', color);
+    },
+
+    tagEl: function() {
+	return this.el?.dom?.children?.[0];
+    },
+
+    iconEl: function() {
+	return this.el?.dom?.children?.[1];
+    },
+
+    initComponent: function() {
+	let me = this;
+	if (me.tag === undefined && !me.addTag) {
+	    throw "no tag given";
+	}
+
+	let mode = 'normal';
+	if (me.addTag) {
+	    me.tag = gettext('Add Tag');
+	    mode = 'addTag';
+	}
+
+	let icon = `<i style="cursor: pointer;" class="fa fa-fw fa-${me.icons[mode]}-circle"></i>`;
+	me.html = `<span>${me.tag}</span> ${icon}`;
+
+	me.callParent();
+	me.updateColor(me.tag);
+	if (me.addTag) {
+	    me.setStyle('cursor', 'pointer');
+	}
+    },
+});
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 08/10] ui: {lxc, qemu}/Config: show Tags and make them editable
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (8 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 07/10] ui: add form/Tag Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 09/10] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 10/10] ui: form/GlobalSearchField: display tags and allow to search for them Dominik Csapak
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

add the tags in the status line, and add a button for adding new ones

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/lxc/Config.js  | 68 +++++++++++++++++++++++++++++++++++--
 www/manager6/qemu/Config.js | 66 +++++++++++++++++++++++++++++++++--
 2 files changed, 130 insertions(+), 4 deletions(-)

diff --git a/www/manager6/lxc/Config.js b/www/manager6/lxc/Config.js
index 89b59c9b..4e2b4bb8 100644
--- a/www/manager6/lxc/Config.js
+++ b/www/manager6/lxc/Config.js
@@ -4,6 +4,8 @@ Ext.define('PVE.lxc.Config', {
 
     onlineHelp: 'chapter_pct',
 
+    userCls: 'proxmox-tags-full',
+
     initComponent: function() {
         var me = this;
 	var vm = me.pveSelNode.data;
@@ -182,12 +184,43 @@ Ext.define('PVE.lxc.Config', {
 	    ],
 	});
 
+	let tagsContainer = Ext.create('Ext.container.Container', {
+	    userCls: 'proxmox-tags-full',
+	    layout: {
+		type: 'hbox',
+		align: 'stretch',
+	    },
+	});
+
+	let tagAddBtn = Ext.create('Proxmox.Tag', {
+	    xtype: 'pmxTag',
+	    addTag: true,
+	    layoutCallback: () => me.query('>toolbar[dock=top]')?.[0].updateLayout(),
+	    updateCallback: function(newTag) {
+		let rec = me.statusStore.data.get('tags');
+		let tags = rec?.data?.value?.split(/[;, ]/) || [];
+		tags.push(newTag);
+		Proxmox.Utils.API2Request({
+		    url: base_url + '/config',
+		    method: 'PUT',
+		    params: {
+			tags: tags.filter(t => !!t).join(','),
+		    },
+		    success: function() {
+			me.statusStore.load();
+		    },
+		    failure: function(response) {
+			Ext.Msg.alert('Error', response.htmlStatus);
+		    },
+		});
+	    },
+	});
 
 	Ext.apply(me, {
 	    title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename),
 	    hstateid: 'lxctab',
 	    tbarSpacing: false,
-	    tbar: [statusTxt, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
+	    tbar: [statusTxt, tagsContainer, '-', tagAddBtn, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
 	    defaults: { statusStore: me.statusStore },
 	    items: [
 		{
@@ -344,10 +377,12 @@ Ext.define('PVE.lxc.Config', {
 	me.mon(me.statusStore, 'load', function(s, records, success) {
 	    var status;
 	    var lock;
+	    var rec;
+
 	    if (!success) {
 		status = 'unknown';
 	    } else {
-		var rec = s.data.get('status');
+		rec = s.data.get('status');
 		status = rec ? rec.data.value : 'unknown';
 		rec = s.data.get('template');
 		template = rec ? rec.data.value : false;
@@ -357,6 +392,35 @@ Ext.define('PVE.lxc.Config', {
 
 	    statusTxt.update({ lock: lock });
 
+	    rec = s.data.get('tags');
+	    if (me.oldtags === undefined || me.oldtags !== rec?.data?.value) {
+		let tags = rec?.data?.value.split(/[,; ]/).filter(t => !!t) ?? [];
+		me.oldtags = rec?.data?.value;
+		tagsContainer.removeAll();
+		for (let i = 0; i < tags.length; i++) {
+		    let tag = tags[i];
+		    tagsContainer.add({
+			xtype: 'pmxTag',
+			tag,
+			layoutCallback: () => tagsContainer.updateLayout(),
+			updateCallback: function(newTag) {
+			    tags[i] = newTag;
+			    Proxmox.Utils.API2Request({
+				url: base_url + '/config',
+				method: 'PUT',
+				params: {
+				    tags: tags.filter(t => !!t).join(','),
+				},
+				failure: function(response) {
+				    Ext.Msg.alert('Error', response.htmlStatus);
+				},
+			    });
+			    me.oldtags = tags.join(',');
+			},
+		    });
+		}
+	    }
+
 	    startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template);
 	    shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
 	    me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js
index 9fe933df..df5f688f 100644
--- a/www/manager6/qemu/Config.js
+++ b/www/manager6/qemu/Config.js
@@ -3,6 +3,7 @@ Ext.define('PVE.qemu.Config', {
     alias: 'widget.PVE.qemu.Config',
 
     onlineHelp: 'chapter_virtual_machines',
+    userCls: 'proxmox-tags-full',
 
     initComponent: function() {
         var me = this;
@@ -219,11 +220,42 @@ Ext.define('PVE.qemu.Config', {
 	    ],
 	});
 
+	let tagsContainer = Ext.create('Ext.container.Container', {
+	    layout: {
+		type: 'hbox',
+		align: 'stretch',
+	    },
+	});
+
+	let tagAddBtn = Ext.create('Proxmox.Tag', {
+	    xtype: 'pmxTag',
+	    addTag: true,
+	    layoutCallback: () => me.query('>toolbar[dock=top]')?.[0].updateLayout(),
+	    updateCallback: function(newTag) {
+		let rec = me.statusStore.data.get('tags');
+		let tags = rec?.data?.value?.split(/[;, ]/) || [];
+		tags.push(newTag);
+		Proxmox.Utils.API2Request({
+		    url: base_url + '/config',
+		    method: 'PUT',
+		    params: {
+			tags: tags.filter(t => !!t).join(','),
+		    },
+		    success: function() {
+			me.statusStore.load();
+		    },
+		    failure: function(response) {
+			Ext.Msg.alert('Error', response.htmlStatus);
+		    },
+		});
+	    },
+	});
+
 	Ext.apply(me, {
 	    title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename),
 	    hstateid: 'kvmtab',
 	    tbarSpacing: false,
-	    tbar: [statusTxt, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
+	    tbar: [statusTxt, tagsContainer, '-', tagAddBtn, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
 	    defaults: { statusStore: me.statusStore },
 	    items: [
 		{
@@ -382,11 +414,12 @@ Ext.define('PVE.qemu.Config', {
 	    var spice = false;
 	    var xtermjs = false;
 	    var lock;
+	    var rec;
 
 	    if (!success) {
 		status = qmpstatus = 'unknown';
 	    } else {
-		var rec = s.data.get('status');
+		rec = s.data.get('status');
 		status = rec ? rec.data.value : 'unknown';
 		rec = s.data.get('qmpstatus');
 		qmpstatus = rec ? rec.data.value : 'unknown';
@@ -399,6 +432,35 @@ Ext.define('PVE.qemu.Config', {
 		xtermjs = !!s.data.get('serial');
 	    }
 
+	    rec = s.data.get('tags');
+	    if (me.oldtags === undefined || me.oldtags !== rec?.data?.value) {
+		let tags = rec?.data?.value.split(/[,; ]/).filter(t => !!t) ?? [];
+		me.oldtags = rec?.data?.value;
+		tagsContainer.removeAll();
+		for (let i = 0; i < tags.length; i++) {
+		    let tag = tags[i];
+		    tagsContainer.add({
+			xtype: 'pmxTag',
+			tag,
+			layoutCallback: () => tagsContainer.updateLayout(),
+			updateCallback: function(newTag) {
+			    tags[i] = newTag;
+			    Proxmox.Utils.API2Request({
+				url: base_url + '/config',
+				method: 'PUT',
+				params: {
+				    tags: tags.filter(t => !!t).join(','),
+				},
+				failure: function(response) {
+				    Ext.Msg.alert('Error', response.htmlStatus);
+				},
+			    });
+			    me.oldtags = tags.join(',');
+			},
+		    });
+		}
+	    }
+
 	    if (template) {
 		return;
 	    }
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 09/10] ui: tree/ResourceTree: show Tags in tree
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (9 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 08/10] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 10/10] ui: form/GlobalSearchField: display tags and allow to search for them Dominik Csapak
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/lxc/Config.js        | 4 +++-
 www/manager6/qemu/Config.js       | 4 +++-
 www/manager6/tree/ResourceTree.js | 4 ++++
 3 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/www/manager6/lxc/Config.js b/www/manager6/lxc/Config.js
index 4e2b4bb8..80c5642b 100644
--- a/www/manager6/lxc/Config.js
+++ b/www/manager6/lxc/Config.js
@@ -216,8 +216,10 @@ Ext.define('PVE.lxc.Config', {
 	    },
 	});
 
+	let vm_text = `${vm.vmid} (${vm.name})`;
+
 	Ext.apply(me, {
-	    title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename),
+	    title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm_text, nodename),
 	    hstateid: 'lxctab',
 	    tbarSpacing: false,
 	    tbar: [statusTxt, tagsContainer, '-', tagAddBtn, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js
index df5f688f..c1865b7f 100644
--- a/www/manager6/qemu/Config.js
+++ b/www/manager6/qemu/Config.js
@@ -251,8 +251,10 @@ Ext.define('PVE.qemu.Config', {
 	    },
 	});
 
+	let vm_text = `${vm.vmid} (${vm.name})`;
+
 	Ext.apply(me, {
-	    title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename),
+	    title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm_text, nodename),
 	    hstateid: 'kvmtab',
 	    tbarSpacing: false,
 	    tbar: [statusTxt, tagsContainer, '-', tagAddBtn, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
diff --git a/www/manager6/tree/ResourceTree.js b/www/manager6/tree/ResourceTree.js
index 301f7557..049194ee 100644
--- a/www/manager6/tree/ResourceTree.js
+++ b/www/manager6/tree/ResourceTree.js
@@ -5,6 +5,8 @@ Ext.define('PVE.tree.ResourceTree', {
     extend: 'Ext.tree.TreePanel',
     alias: ['widget.pveResourceTree'],
 
+    userCls: 'proxmox-tags-circle',
+
     statics: {
 	typeDefaults: {
 	    node: {
@@ -114,6 +116,8 @@ Ext.define('PVE.tree.ResourceTree', {
 	    }
 	}
 
+	info.text += PVE.Utils.renderTags(info.tags);
+
 	info.text = status + info.text;
     },
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

* [pve-devel] [PATCH manager v4 10/10] ui: form/GlobalSearchField: display tags and allow to search for them
  2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
                   ` (10 preceding siblings ...)
  2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 09/10] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
@ 2022-03-23 10:34 ` Dominik Csapak
  11 siblings, 0 replies; 13+ messages in thread
From: Dominik Csapak @ 2022-03-23 10:34 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/form/GlobalSearchField.js | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/www/manager6/form/GlobalSearchField.js b/www/manager6/form/GlobalSearchField.js
index 267a480d..7a27cb29 100644
--- a/www/manager6/form/GlobalSearchField.js
+++ b/www/manager6/form/GlobalSearchField.js
@@ -15,6 +15,7 @@ Ext.define('PVE.form.GlobalSearchField', {
 
     grid: {
 	xtype: 'gridpanel',
+	userCls: 'proxmox-tags-full',
 	focusOnToFront: false,
 	floating: true,
 	emptyText: Proxmox.Utils.noneText,
@@ -23,7 +24,7 @@ Ext.define('PVE.form.GlobalSearchField', {
 	scrollable: {
 	    xtype: 'scroller',
 	    y: true,
-	    x: false,
+	    x: true,
 	},
 	store: {
 	    model: 'PVEResources',
@@ -78,6 +79,10 @@ Ext.define('PVE.form.GlobalSearchField', {
 		text: gettext('Description'),
 		flex: 1,
 		dataIndex: 'text',
+		renderer: function(value, mD, rec) {
+		    let tags = PVE.Utils.renderTags(rec.data.tags);
+		    return `${value}${tags}`;
+		},
 	    },
 	    {
 		text: gettext('Node'),
@@ -104,16 +109,20 @@ Ext.define('PVE.form.GlobalSearchField', {
 	    'storage': ['type', 'pool', 'node', 'storage'],
 	    'default': ['name', 'type', 'node', 'pool', 'vmid'],
 	};
-	let fieldArr = fieldMap[item.data.type] || fieldMap.default;
+	let fields = fieldMap[item.data.type] || fieldMap.default;
+	let fieldArr = fields.map(field => item.data[field]?.toString().toLowerCase());
+	if (item.data.tags) {
+	    let tags = item.data.tags.split(/[;, ]/);
+	    fieldArr.push(...tags);
+	}
 
 	let filterWords = me.filterVal.split(/\s+/);
 
 	// all text is case insensitive and each split-out word is searched for separately.
 	// a row gets 1 point for every partial match, and and additional point for every exact match
 	let match = 0;
-	for (let field of fieldArr) {
-	    let fieldValue = item.data[field]?.toString().toLowerCase();
-	    if (fieldValue === undefined) {
+	for (let fieldValue of fieldArr) {
+	    if (fieldValue === undefined || fieldValue === "") {
 		continue;
 	    }
 	    for (let filterWord of filterWords) {
-- 
2.30.2





^ permalink raw reply	[flat|nested] 13+ messages in thread

end of thread, other threads:[~2022-03-23 10:35 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-03-23 10:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v4] add tags to ui Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH cluster v4 1/1] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH widget-toolkit v4 1/1] add tag related helpers Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 01/10] api: /cluster/resources: add tags to returned properties Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 02/10] api: /version: add 'tag-colors' and 'tag-tree-style' Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 03/10] ui: parse and save tag color overrides from /version Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 04/10] ui: tree/ResourceTree: collect tags on update Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 05/10] ui: add form/TagColorGrid Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 06/10] ui: dc/OptionView: add editors for tag settings Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 07/10] ui: add form/Tag Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 08/10] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 09/10] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
2022-03-23 10:34 ` [pve-devel] [PATCH manager v4 10/10] ui: form/GlobalSearchField: display tags and allow to search for them Dominik Csapak

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