public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui
@ 2022-04-08  7:45 Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH cluster v5 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
                   ` (15 more replies)
  0 siblings, 16 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 UTC (permalink / raw)
  To: pve-devel

this series brings the already existing 'tags' for ct/vms to the gui:
* tags can be edited in the status toolbar of the guest
* existing tags will be shown in the tree/global search
* when editing a tag, a list of existing tags will be shown
* by default, the color is (consistently) autogenerated based on the
  text
* that color can be overriden in datacenter -> options (cluster wide)
  (edit window for browser local storage is TBD)
* by default, the text color is either white or black, depending which
  provides the greater contrast (according to SAPC)
* this text color can also be overridden
* there are multiple styles available for the tree
  (see [0])

changes from v4:
* optimized the global taglist/tagoverrides so that they don't have to
  be copied for everytag, but only once on update
* made the tags less round by default
* to edit, one must first enter 'edit' mode by clicking on an edit icon
  and apply by clicking on an apply button
  (this way one can copy&paste the tags without starting an edit, and
  can edit multiple tags with only one api call)
* improved the 'dense' style a bit (wider + spacing)
* includes all necessary cluster patches

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://imgur.com/a/0t2fvud

pve-cluster:

Dominik Csapak (3):
  add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  Cluster: add get_guest_config_properties
  datacenter.cfg: add option for tag-tree-style and tag-colors

 data/PVE/Cluster.pm          |  27 ++++++
 data/PVE/DataCenterConfig.pm |  18 ++++
 data/src/cfs-ipc-ops.h       |   2 +
 data/src/server.c            |  62 +++++++++++++
 data/src/status.c            | 174 ++++++++++++++++++++++++-----------
 data/src/status.h            |   3 +
 6 files changed, 230 insertions(+), 56 deletions(-)

proxmox-widget-toolkit:

Dominik Csapak (1):
  add tag related helpers

 src/Utils.js         | 70 ++++++++++++++++++++++++++++++++++++++++++++
 src/css/ext6-pmx.css | 45 ++++++++++++++++++++++++++++
 2 files changed, 115 insertions(+)

pve-manager:

Dominik Csapak (11):
  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: add form/TagEdit.js
  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/css/ext6-pve.css                   |   5 +
 www/manager6/Makefile                  |   3 +
 www/manager6/Utils.js                  |  68 ++++++
 www/manager6/Workspace.js              |  13 ++
 www/manager6/data/ResourceStore.js     |   6 +
 www/manager6/dc/OptionView.js          |  43 +++-
 www/manager6/form/GlobalSearchField.js |  20 +-
 www/manager6/form/Tag.js               | 254 ++++++++++++++++++++
 www/manager6/form/TagColorGrid.js      | 309 +++++++++++++++++++++++++
 www/manager6/form/TagEdit.js           | 131 +++++++++++
 www/manager6/lxc/Config.js             |  36 ++-
 www/manager6/qemu/Config.js            |  35 ++-
 www/manager6/tree/ResourceTree.js      |  20 +-
 15 files changed, 946 insertions(+), 18 deletions(-)
 create mode 100644 www/manager6/form/Tag.js
 create mode 100644 www/manager6/form/TagColorGrid.js
 create mode 100644 www/manager6/form/TagEdit.js

-- 
2.30.2





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

* [pve-devel] [PATCH cluster v5 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08 10:05   ` Matthias Heiserer
  2022-04-08  7:45 ` [pve-devel] [PATCH cluster v5 2/3] Cluster: add get_guest_config_properties Dominik Csapak
                   ` (14 subsequent siblings)
  15 siblings, 1 reply; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 UTC (permalink / raw)
  To: pve-devel

for getting multiple properties from the in memory config of the
guests. I added a new CSF_IPC_ call to maintain backwards compatibility.

It basically behaves the same as
CFS_IPC_GET_GUEST_CONFIG_PROPERTY, but takes a list of properties
instead.

The old way of getting a single property is now also done by
the new function.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 data/src/cfs-ipc-ops.h |   2 +
 data/src/server.c      |  62 +++++++++++++++
 data/src/status.c      | 174 ++++++++++++++++++++++++++++-------------
 data/src/status.h      |   3 +
 4 files changed, 185 insertions(+), 56 deletions(-)

diff --git a/data/src/cfs-ipc-ops.h b/data/src/cfs-ipc-ops.h
index 003e233..249308d 100644
--- a/data/src/cfs-ipc-ops.h
+++ b/data/src/cfs-ipc-ops.h
@@ -43,4 +43,6 @@
 
 #define CFS_IPC_VERIFY_TOKEN 12
 
+#define CFS_IPC_GET_GUEST_CONFIG_PROPERTIES 13
+
 #endif
diff --git a/data/src/server.c b/data/src/server.c
index 549788a..b9394c8 100644
--- a/data/src/server.c
+++ b/data/src/server.c
@@ -89,6 +89,13 @@ typedef struct {
 	char property[];
 } cfs_guest_config_propery_get_request_header_t;
 
+typedef struct {
+	struct qb_ipc_request_header req_header;
+	uint32_t vmid;
+	uint8_t num_props;
+	char props[]; /* list of \0 terminated properties */
+} cfs_guest_config_properties_get_request_header_t;
+
 typedef struct {
 	struct qb_ipc_request_header req_header;
 	char token[];
@@ -348,6 +355,61 @@ static int32_t s1_msg_process_fn(
 
 			result = cfs_create_guest_conf_property_msg(outbuf, memdb, rh->property, rh->vmid);
 		}
+	} else if (request_id == CFS_IPC_GET_GUEST_CONFIG_PROPERTIES) {
+
+		cfs_guest_config_properties_get_request_header_t *rh =
+			(cfs_guest_config_properties_get_request_header_t *) data;
+
+		int propslen = request_size - G_STRUCT_OFFSET(cfs_guest_config_properties_get_request_header_t, props);
+
+		result = 0;
+		if (rh->vmid < 100 && rh->vmid != 0) {
+			cfs_debug("vmid out of range %u", rh->vmid);
+			result = -EINVAL;
+		} else if (rh->vmid >= 100 && !vmlist_vm_exists(rh->vmid)) {
+			result = -ENOENT;
+		} else if (propslen <= 1) {
+			cfs_debug("propslen <= 1, %d", propslen);
+			result = -EINVAL;
+		} else {
+			const char **properties = malloc(sizeof(char*) * rh->num_props);
+			char *current = (rh->props);
+			int remaining = propslen;
+			for (uint8_t i = 0; i < rh->num_props; i++) {
+			    uint8_t proplen = strnlen(current, remaining);
+			    if (proplen == 0) {
+				cfs_debug("property length 0");
+				result = -EINVAL;
+				break;
+			    }
+			    if (proplen == remaining) {
+				cfs_debug("property not \\0 terminated");
+				result = -EINVAL;
+				break;
+			    }
+			    if (current[0] < 'a' || current[0] > 'z') {
+				cfs_debug("property does not start with [a-z]");
+				result = -EINVAL;
+				break;
+			    }
+			    properties[i] = current;
+			    current[proplen] = '\0'; // ensure property is 0 terminated
+			    remaining -= (proplen + 1);
+			    current += proplen + 1;
+			}
+
+			if (remaining != 0) {
+			    cfs_debug("leftover data after parsing %ul properties", rh->num_props);
+			    result = -EINVAL;
+			}
+
+			if (result == 0) {
+			    cfs_debug("cfs_get_guest_config_properties: basic valid checked, do request");
+			    result = cfs_create_guest_conf_properties_msg(outbuf, memdb, properties, rh->num_props, rh->vmid);
+			}
+
+			free(properties);
+		}
 	} else if (request_id == CFS_IPC_VERIFY_TOKEN) {
 
 		cfs_verify_token_request_header_t *rh = (cfs_verify_token_request_header_t *) data;
diff --git a/data/src/status.c b/data/src/status.c
index 9bceaeb..33ca06f 100644
--- a/data/src/status.c
+++ b/data/src/status.c
@@ -804,25 +804,52 @@ cfs_create_vmlist_msg(GString *str)
 	return 0;
 }
 
-// checks the conf for a line starting with '$prop:' and returns the value
-// afterwards, whitout initial whitespace(s), we only deal with the format
-// restricion imposed by our perl VM config parser, main reference is
+// checks if a config line starts with the given prop. if yes, writes a '\0'
+// at the end of the value, and returns the pointer where the value starts
+char *
+_get_property_value_from_line(char *line, int line_len, const char *prop, int prop_len)
+{
+	if (line_len <= prop_len + 1) return NULL;
+
+	if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
+		char *v_start = &line[prop_len + 1];
+
+		// drop initial value whitespaces here already
+		while (*v_start && isspace(*v_start)) v_start++;
+
+		if (!*v_start) return NULL;
+
+		char *v_end = &line[line_len - 1];
+		while (v_end > v_start && isspace(*v_end)) v_end--;
+		v_end[1] = '\0';
+
+		return v_start;
+	}
+
+	return NULL;
+}
+
+// checks the conf for lines starting with the given props and
+// writes the pointers into the correct positions into the 'found' array
+// afterwards, whithout initial whitespace(s), we only deal with the format
+// restriction imposed by our perl VM config parser, main reference is
 // PVE::QemuServer::parse_vm_config this allows to be very fast and still
 // relatively simple
-// main restrictions used for our advantage is the properties match reges:
+// main restrictions used for our advantage is the properties match regex:
 // ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) from parse_vm_config
 // currently we only look at the current configuration in place, i.e., *no*
-// snapshort and *no* pending changes
-static char *
-_get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
+// snapshot and *no* pending changes
+void
+_get_property_values(char **found, char *conf, int conf_size, const char **props, uint8_t num_props, char min, char max)
 {
 	const char *const conf_end = conf + conf_size;
 	char *line = conf;
-	size_t remaining_size;
+	size_t remaining_size = conf_size;
+	int count = 0;
 
 	char *next_newline = memchr(conf, '\n', conf_size);
 	if (next_newline == NULL) {
-		return NULL; // valid property lines end with \n, but none in the config
+		return; // valid property lines end with \n, but none in the config
 	}
 	*next_newline = '\0';
 
@@ -830,41 +857,33 @@ _get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
 		if (!line[0]) goto next;
 
 		// snapshot or pending section start, but nothing found yet -> not found
-		if (line[0] == '[') return NULL;
-		// properties start with /^[a-z]/, so continue early if not
-		if (line[0] < 'a' || line[0] > 'z') goto next;
+		if (line[0] == '[') return;
+		// continue early if line does not begin with the min/max char of the properties
+		if (line[0] < min || line[0] > max) goto next;
 
 		int line_len = strlen(line);
-		if (line_len <= prop_len + 1) goto next;
-
-		if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
-			char *v_start = &line[prop_len + 1];
-
-			// drop initial value whitespaces here already
-			while (*v_start && isspace(*v_start)) v_start++;
-
-			if (!*v_start) return NULL;
-
-			char *v_end = &line[line_len - 1];
-			while (v_end > v_start && isspace(*v_end)) v_end--;
-			v_end[1] = '\0';
-
-			return v_start;
+		for (uint8_t i = 0; i < num_props; i++) {
+			char * value = _get_property_value_from_line(line, line_len, props[i], strlen(props[i]));
+			if (value != NULL) {
+				count += (found[i] != NULL) & 0x1; // count newly found lines
+				found[i] = value;
+			}
+		}
+		if (count == num_props) {
+			// found all
+			return;
 		}
 next:
 		line = next_newline + 1;
 		remaining_size = conf_end - line;
-		if (remaining_size <= prop_len) {
-			return NULL;
-		}
 		next_newline = memchr(line, '\n', remaining_size);
 		if (next_newline == NULL) {
-			return NULL; // valid property lines end with \n, but none in the config
+			return;
 		}
 		*next_newline = '\0';
 	}
 
-	return NULL; // not found
+	return;
 }
 
 static void
@@ -883,24 +902,73 @@ _g_str_append_kv_jsonescaped(GString *str, const char *k, const char *v)
 }
 
 int
-cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
+_print_found_properties(
+	GString *str,
+	gpointer conf,
+	int size,
+	const char **props,
+	uint8_t num_props,
+	uint32_t vmid,
+	char **values,
+	char min,
+	char max,
+	int first)
+{
+	_get_property_values(values, conf, size, props, num_props, min, max);
+
+	uint8_t found = 0;
+	for (uint8_t i = 0; i < num_props; i++) {
+		if (values[i] != NULL) {
+			if (found) {
+				g_string_append_c(str, ',');
+			} else {
+				if (!first) g_string_append_printf(str, ",\n");
+				else first = 0;
+				g_string_append_printf(str, "\"%u\":{", vmid);
+				found = 1;
+			}
+			_g_str_append_kv_jsonescaped(str, props[i], values[i]);
+		}
+	}
+
+	if (found) {
+		g_string_append_c(str, '}');
+	}
+
+	return first;
+}
+
+int
+cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid)
 {
 	g_return_val_if_fail(cfs_status.vmlist != NULL, -EINVAL);
 	g_return_val_if_fail(str != NULL, -EINVAL);
 
-	int prop_len = strlen(prop);
-	int res = 0;
-	GString *path = NULL;
-
 	// Prelock &memdb->mutex in order to enable the usage of memdb_read_nolock
 	// to prevent Deadlocks as in #2553
 	g_mutex_lock (&memdb->mutex);
 	g_mutex_lock (&mutex);
 
-	g_string_printf(str,"{\n");
+	g_string_printf(str, "{\n");
 
 	GHashTable *ht = cfs_status.vmlist;
+
+	int res = 0;
+	GString *path = NULL;
 	gpointer tmp = NULL;
+	char **values = calloc(num_props, sizeof(char*));
+	char min = 'z', max = 'a';
+
+	for (uint8_t i = 0; i < num_props; i++) {
+		if (props[i][0] > max) {
+			max = props[i][0];
+		}
+
+		if (props[i][0] < min) {
+			min = props[i][0];
+		}
+	}
+
 	if (!g_hash_table_size(ht)) {
 		goto ret;
 	}
@@ -919,15 +987,8 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
 		// use memdb_read_nolock because lock is handled here
 		int size = memdb_read_nolock(memdb, path->str, &tmp);
 		if (tmp == NULL) goto err;
-		if (size <= prop_len) goto ret;
-
-		char *val = _get_property_value(tmp, size, prop, prop_len);
-		if (val == NULL) goto ret;
-
-		g_string_append_printf(str, "\"%u\":{", vmid);
-		_g_str_append_kv_jsonescaped(str, prop, val);
-		g_string_append_c(str, '}');
 
+		_print_found_properties(str, tmp, size, props, num_props, vmid, values, min, max, 1);
 	} else {
 		GHashTableIter iter;
 		g_hash_table_iter_init (&iter, ht);
@@ -943,21 +1004,16 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
 			tmp = NULL;
 			// use memdb_read_nolock because lock is handled here
 			int size = memdb_read_nolock(memdb, path->str, &tmp);
-			if (tmp == NULL || size <= prop_len) continue;
+			if (tmp == NULL) continue;
 
-			char *val = _get_property_value(tmp, size, prop, prop_len);
-			if (val == NULL) continue;
-
-			if (!first) g_string_append_printf(str, ",\n");
-			else first = 0;
-
-			g_string_append_printf(str, "\"%u\":{", vminfo->vmid);
-			_g_str_append_kv_jsonescaped(str, prop, val);
-			g_string_append_c(str, '}');
+			memset(values, 0, sizeof(char*)*num_props); // reset array
+			first = _print_found_properties(str, tmp, size, props, num_props,
+					vminfo->vmid, values, min, max, first);
 		}
 	}
 ret:
 	g_free(tmp);
+	free(values);
 	if (path != NULL) {
 		g_string_free(path, TRUE);
 	}
@@ -973,6 +1029,12 @@ enoent:
 	goto ret;
 }
 
+int
+cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
+{
+	return cfs_create_guest_conf_properties_msg(str, memdb, &prop, 1, vmid);
+}
+
 void
 record_memdb_change(const char *path)
 {
diff --git a/data/src/status.h b/data/src/status.h
index bbf0948..041cb34 100644
--- a/data/src/status.h
+++ b/data/src/status.h
@@ -163,4 +163,7 @@ cfs_create_memberlist_msg(
 int
 cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid);
 
+int
+cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid);
+
 #endif /* _PVE_STATUS_H_ */
-- 
2.30.2





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

* [pve-devel] [PATCH cluster v5 2/3] Cluster: add get_guest_config_properties
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH cluster v5 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH cluster v5 3/3] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
                   ` (13 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 UTC (permalink / raw)
  To: pve-devel

akin to get_guest_config_property, but with a list of properties.
uses the new CFS_IPC_GET_GUEST_CONFIG_PROPERTIES

also adds the same NOTEs regarding parsing/permissions to the comment
of get_guest_config_property

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

diff --git a/data/PVE/Cluster.pm b/data/PVE/Cluster.pm
index 05451fd..d0148fc 100644
--- a/data/PVE/Cluster.pm
+++ b/data/PVE/Cluster.pm
@@ -340,10 +340,37 @@ sub get_node_kv {
     return $res;
 }
 
+# properties: an array-ref of config properties you want to get, e.g., this
+# is perfect to get multiple properties of a guest _fast_
+# (>100 faster than manual parsing here)
+# vmid: optional, if a valid is passed we only check that one, else return all
+# NOTE: does *not* searches snapshot and PENDING entries sections!
+# NOTE: returns the guest config lines (excluding trailing whitespace) as is,
+#       so for non-trivial properties, checking the validity must be done
+# NOTE: no permission check is done, that is the responsibilty of the caller
+sub get_guest_config_properties {
+    my ($properties, $vmid) = @_;
+
+    die "properties required" if !defined($properties);
+
+    my $num_props = scalar(@$properties);
+    die "only up to 255 properties supported" if $num_props > 255;
+    my $bindata = pack "VC", $vmid // 0, $num_props;
+    for my $property (@$properties) {
+	$bindata .= pack "Z*", $property;
+    }
+    my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTIES, $bindata);
+
+    return $res;
+}
+
 # property: a config property you want to get, e.g., this is perfect to get
 # the 'lock' entry of a guest _fast_ (>100 faster than manual parsing here)
 # vmid: optional, if a valid is passed we only check that one, else return all
 # NOTE: does *not* searches snapshot and PENDING entries sections!
+# NOTE: returns the guest config lines (excluding trailing whitespace) as is,
+#       so for non-trivial properties, checking the validity must be done
+# NOTE: no permission check is done, that is the responsibilty of the caller
 sub get_guest_config_property {
     my ($property, $vmid) = @_;
 
-- 
2.30.2





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

* [pve-devel] [PATCH cluster v5 3/3] datacenter.cfg: add option for tag-tree-style and tag-colors
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH cluster v5 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH cluster v5 2/3] Cluster: add get_guest_config_properties Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH widget-toolkit v5 1/1] add tag related helpers Dominik Csapak
                   ` (12 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 UTC (permalink / raw)
  To: pve-devel

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

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 6c0fa5b..c21dbb7 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -106,6 +106,10 @@ sub pve_verify_mac_prefix {
     return $mac_prefix;
 }
 
+my $TAG_RE = '[a-zA-Z0-9_][a-zA-Z0-9_\-\+\.]*';
+my $COLOR_RE = '[0-9a-fA-F]{6}';
+my $OVERRIDE_RE = "(?:${TAG_RE}=${COLOR_RE}(?:\:${COLOR_RE})?)";
+
 my $datacenter_schema = {
     type => "object",
     additionalProperties => 0,
@@ -222,6 +226,20 @@ 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 => "${OVERRIDE_RE}(?:\,$OVERRIDE_RE)*",
+	    typetext => '<tag>=<hex-color>[:<hex-color-for-text>][,<tag>=...]',
+	    description => "Manual color mapping for tags (comma separated).",
+	},
     },
 };
 
-- 
2.30.2





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

* [pve-devel] [PATCH widget-toolkit v5 1/1] add tag related helpers
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (2 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH cluster v5 3/3] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 01/11] api: /cluster/resources: add tags to returned properties Dominik Csapak
                   ` (11 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 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

contrast is calculated according to SAPC draft:
https://github.com/Myndex/SAPC-APCA

which is likely to become a w3c guideline in the future and seems
to be a better algorithm for this

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

diff --git a/src/Utils.js b/src/Utils.js
index 6a03057..09085d8 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1272,6 +1272,76 @@ 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
+	}
+
+	let alpha = 0.7; // make the color a bit brighter
+	let bg = 255; // assume white background
+
+	return [
+	    (hash & 255)*alpha + bg*(1-alpha),
+	    ((hash >> 8) & 255)*alpha + bg*(1-alpha),
+	    ((hash >> 16) & 255)*alpha + bg*(1-alpha),
+	];
+    },
+
+    rgbToCss: function(rgb) {
+	return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
+    },
+
+    // optimized & simplified SAPC function
+    // https://github.com/Myndex/SAPC-APCA
+    getTextContrastClass: function(rgb) {
+	    const blkThrs = 0.022;
+	    const blkClmp = 1.414;
+
+	    // linearize & gamma correction
+	    let r = (rgb[0]/255)**2.4;
+	    let g = (rgb[1]/255)**2.4;
+	    let b = (rgb[2]/255)**2.4;
+
+	    // relative luminance sRGB
+	    let bg = r*0.2126729 + g*0.7151522 + b*0.0721750;
+
+	    // black clamp
+	    bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp;
+
+	    // SAPC with white text
+	    let contrastLight = bg ** 0.65 - 1;
+	    // SAPC with black text
+	    let contrastDark = bg ** 0.56 - 0.046134502;
+
+	    if (Math.abs(contrastLight) >= Math.abs(contrastDark)) {
+		return 'light';
+	    } else {
+		return 'dark';
+	    }
+    },
+
+    getTagElement: function(string, color_overrides) {
+	let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string);
+	let bgcolor = Proxmox.Utils.rgbToCss(rgb);
+	let style = `background-color: ${bgcolor};`;
+	let cls;
+	if (rgb.length > 3) {
+	    let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]);
+	    style += `color: ${fgcolor}`;
+	    cls = "proxmox-tag-dark";
+	} else {
+	    let txtCls = Proxmox.Utils.getTextContrastClass(rgb);
+	    cls = `proxmox-tag-${txtCls}`;
+	}
+	return `<span class="${cls}" style="${style}">${string}</span>`;
+    },
 },
 
     singleton: true,
diff --git a/src/css/ext6-pmx.css b/src/css/ext6-pmx.css
index 2516578..4448751 100644
--- a/src/css/ext6-pmx.css
+++ b/src/css/ext6-pmx.css
@@ -6,6 +6,51 @@
     background-color: LightYellow;
 }
 
+.proxmox-tags-full .proxmox-tag-light,
+.proxmox-tags-full .proxmox-tag-dark {
+    border-radius: 3px;
+    padding: 1px 6px;
+    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 !important;
+    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: 6px;
+    margin-right: 1px;
+    display: inline-block;
+    color: transparent !important;
+    overflow: hidden;
+    vertical-align: bottom;
+}
+
+.proxmox-tags-full .proxmox-tag-light {
+    color: #fff;
+    background-color: #383838;
+}
+
+.proxmox-tags-full .proxmox-tag-dark {
+    color: #000;
+    background-color: #f0f0f0;
+}
+
 .x-mask-msg-text {
     text-align: center;
 }
-- 
2.30.2





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

* [pve-devel] [PATCH manager v5 01/11] api: /cluster/resources: add tags to returned properties
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (3 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH widget-toolkit v5 1/1] add tag related helpers Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 02/11] api: /version: add 'tag-colors' and 'tag-tree-style' Dominik Csapak
                   ` (10 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 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 718a8eb9..01a21c9e 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] 19+ messages in thread

* [pve-devel] [PATCH manager v5 02/11] api: /version: add 'tag-colors' and 'tag-tree-style'
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (4 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 01/11] api: /cluster/resources: add tags to returned properties Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 03/11] ui: parse and save tag color overrides from /version Dominik Csapak
                   ` (9 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 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] 19+ messages in thread

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

into a global list of overrides. on update, also parse the values
from the browser localstore

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

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 735806aa..a0a60cd1 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1800,6 +1800,47 @@ 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];
+	    if (color_hex.length === 13) {
+		colors[tag].push(parseInt(color_hex.slice(7, 9), 16));
+		colors[tag].push(parseInt(color_hex.slice(9, 11), 16));
+		colors[tag].push(parseInt(color_hex.slice(11, 13), 16));
+	    }
+	});
+	return colors;
+    },
+
+    tagOverrides: {},
+
+    updateTagOverrides: function(colors) {
+	let sp = Ext.state.Manager.getProvider();
+	let color_state = sp.get('colors', '');
+	let browser_colors = PVE.Utils.parseTagOverrides(color_state);
+	PVE.Utils.tagOverrides = Ext.apply({}, browser_colors, colors);
+    },
+
+    updateTagSettings: function(overrides, style) {
+	if (overrides) {
+	    PVE.Utils.updateTagOverrides(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] 19+ messages in thread

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

into a global list, so that we have it avaiable anywhere
also add the tags from the tagOverrides on update into the list

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

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index a0a60cd1..be9d96fa 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1801,6 +1801,13 @@ Ext.define('PVE.Utils', {
 	return undefined;
     },
 
+    tagList: new Set(),
+
+    updateTagList: function(tags) {
+	let override_tags = Object.keys(PVE.Utils.tagOverrides);
+	PVE.Utils.tagList = [...new Set([...tags, ...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..139defab 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.updateTagList(tags);
 	    store.resumeEvents();
 	    store.fireEvent('refresh', store);
 
-- 
2.30.2





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

* [pve-devel] [PATCH manager v5 05/11] ui: add form/TagColorGrid
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (7 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 04/11] ui: tree/ResourceTree: collect tags on update Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08 11:39   ` Matthias Heiserer
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 06/11] ui: dc/OptionView: add editors for tag settings Dominik Csapak
                   ` (6 subsequent siblings)
  15 siblings, 1 reply; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 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/css/ext6-pve.css              |   5 +
 www/manager6/Makefile             |   1 +
 www/manager6/form/TagColorGrid.js | 309 ++++++++++++++++++++++++++++++
 3 files changed, 315 insertions(+)
 create mode 100644 www/manager6/form/TagColorGrid.js

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index dadb84a9..f7d0c420 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -651,3 +651,8 @@ table.osds td:first-of-type {
     background-color: rgb(245, 245, 245);
     color: #000;
 }
+
+.x-pveColorPicker-default-cell > .x-grid-cell-inner {
+    padding-top: 0px;
+    padding-bottom: 0px;
+}
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..3bd7f419
--- /dev/null
+++ b/www/manager6/form/TagColorGrid.js
@@ -0,0 +1,309 @@
+Ext.define('PVE.form.ColorPicker', {
+    extend: 'Ext.form.FieldContainer',
+    alias: 'widget.pveColorPicker',
+
+    defaultBindProperty: 'value',
+
+    config: {
+	value: null,
+    },
+
+    height: 24,
+
+    layout: {
+	type: 'hbox',
+	align: 'stretch',
+    },
+
+    getValue: function() {
+	return this.realvalue.slice(1);
+    },
+
+    setValue: function(value) {
+	let me = this;
+	me.setColor(value);
+	if (value && value.length === 6) {
+	    me.picker.value = value[0] !== '#' ? `#${value}` : value;
+	}
+    },
+
+    setColor: function(value) {
+	let me = this;
+	let oldValue = me.realvalue;
+	me.realvalue = value;
+	let color = value.length === 6 ? `#${value}` : undefined;
+	me.down('#picker').setStyle('background-color', color);
+	me.down('#text').setValue(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.items = [
+	    {
+		xtype: 'textfield',
+		itemId: 'text',
+		minLength: !me.allowBlank ? 6 : undefined,
+		maxLength: 6,
+		enforceMaxLength: true,
+		allowBlank: me.allowBlank,
+		emptyText: me.allowBlank ? gettext('Automatic') : undefined,
+		maskRe: /[a-f0-9]/i,
+		regex: /^[a-f0-9]{6}$/i,
+		flex: 1,
+		listeners: {
+		    change: function(field, value) {
+			me.setValue(value);
+		    },
+		},
+	    },
+	    {
+		xtype: 'box',
+		style: {
+		    'margin-left': '1px',
+		    border: '1px solid #cfcfcf',
+		},
+		itemId: 'picker',
+		width: 24,
+		contentEl: me.picker,
+	    },
+	];
+
+	me.callParent();
+	me.picker.oninput = function() {
+	    me.setColor(me.picker.value.slice(1));
+	};
+    },
+});
+
+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(/[=]/);
+	    let bg = color;
+	    let fg = "";
+	    if (color.length > 6) {
+		[bg, fg] = color.split(/[:]/);
+	    }
+	    return {
+		tag,
+		color: bg,
+		text: fg,
+	    };
+	});
+	me.getStore().setData(entries);
+	me.checkChange();
+	return me;
+    },
+
+    getValue: function() {
+	let me = this;
+	let values = [];
+	me.getStore().each((rec) => {
+	    if (rec.data.tag) {
+		let val = `${rec.data.tag}=${rec.data.color}`;
+		if (rec.data.text) {
+		    val += `:${rec.data.text}`;
+		}
+		values.push(val);
+	    }
+	});
+	return values.join(',');
+    },
+
+    getErrors: function(value) {
+	let me = this;
+	let emptyTag = false;
+	let notValidColor = false;
+	let colorRegex = new RegExp(/^[0-9a-f]{6}$/i);
+	me.getStore().each((rec) => {
+	    if (!rec.data.tag) {
+		emptyTag = true;
+	    }
+	    if (!rec.data.color?.match(colorRegex)) {
+		notValidColor = true;
+	    }
+	    if (rec.data.text && !rec.data.text?.match(colorRegex)) {
+		notValidColor = true;
+	    }
+	});
+	let errors = [];
+	if (emptyTag) {
+	    errors.push(gettext('Tag must not be empty.'));
+	}
+	if (notValidColor) {
+	    errors.push(gettext('Not a valid color.'));
+	}
+	return errors;
+    },
+
+    // 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({
+		tag: '',
+		color: '',
+		text: '',
+	    });
+	},
+
+	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();
+	    if (!rec) {
+		return;
+	    }
+	    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.tagList.map(v => ({ tag: v })));
+	    },
+	    widget: {
+		xtype: 'combobox',
+		isFormField: false,
+		maskRe: /[a-zA-Z0-9_.-]/,
+		allowBlank: false,
+		queryMode: 'local',
+		displayField: 'tag',
+		valueField: 'tag',
+		store: {},
+		listeners: {
+		    change: 'fieldChange',
+		},
+	    },
+	    flex: 1,
+	},
+	{
+	    header: gettext('Background'),
+	    xtype: 'widgetcolumn',
+	    flex: 1,
+	    dataIndex: 'color',
+	    widget: {
+		xtype: 'pveColorPicker',
+		isFormField: false,
+		listeners: {
+		    change: 'fieldChange',
+		},
+	    },
+	},
+	{
+	    header: gettext('Text'),
+	    xtype: 'widgetcolumn',
+	    flex: 1,
+	    dataIndex: 'text',
+	    widget: {
+		xtype: 'pveColorPicker',
+		allowBlank: true,
+		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] 19+ messages in thread

* [pve-devel] [PATCH manager v5 06/11] ui: dc/OptionView: add editors for tag settings
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (8 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 05/11] ui: add form/TagColorGrid Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 07/11] ui: add form/Tag Dominik Csapak
                   ` (5 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 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 be9d96fa..15e868b9 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1848,6 +1848,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, overrides) {
+	let text = '';
+	if (tagstext) {
+	    let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
+	    text += ' ';
+	    tags.forEach((tag) => {
+		text += Proxmox.Utils.getTagElement(tag, overrides);
+	    });
+	}
+	return text;
+    },
 },
 
     singleton: true,
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index 6b30ede9..d218055b 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: 800,
+		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] 19+ messages in thread

* [pve-devel] [PATCH manager v5 07/11] ui: add form/Tag
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (9 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 06/11] ui: dc/OptionView: add editors for tag settings Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 08/11] ui: add form/TagEdit.js Dominik Csapak
                   ` (4 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 UTC (permalink / raw)
  To: pve-devel

displays a single tag, with the ability to edit inline on click (when
the mode is set to editable). This brings up a list of globally available tags
for simple selection.

Also has a mode for adding a new Tag.

This has 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 | 254 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 255 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..fc078ff3
--- /dev/null
+++ b/www/manager6/form/Tag.js
@@ -0,0 +1,254 @@
+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 layout in the containing element
+    // this is necessary since we circumvent extjs layout with 'contentEditable'
+    layoutCallback: Ext.emptyFn,
+
+    style: {
+	'white-space': 'nowrap',
+    },
+
+    icons: {
+	addTag: 'plus',
+	editable: 'minus',
+	normal: '',
+	edit: 'check',
+    },
+
+    faIconStyle: '-square',
+
+    mode: 'normal',
+
+    // 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') {
+	    return;
+	}
+	switch (me.mode) {
+	    case 'editable':
+		if (me.addTag) {
+		    break;
+		}
+		// "delete" ourselves
+		me.setVisible(false);
+		me.setText('');
+		me.finishEdit();
+		break;
+	    case 'edit':
+		me.finishEdit();
+		break;
+	    default: break;
+	}
+    },
+
+    onClick: function(event) {
+	let me = this;
+	if (event.target.tagName !== 'SPAN' && !me.addTag) {
+	    return;
+	}
+	if (me.mode === 'editable') {
+	    me.startEdit();
+	}
+    },
+
+    startEdit: function() {
+	let me = this;
+	me.setMode('edit');
+
+	// 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-full',
+		displayField: 'tag',
+		itemTpl: [
+		    '{[Proxmox.Utils.getTagElement(values.tag, PVE.Utils.tagOverrides)]}',
+		],
+		store: [],
+		listeners: {
+		    select: function(picker, rec) {
+			me.setText(rec.data.tag);
+			me.finishEdit();
+		    },
+		},
+	    });
+	}
+	me.picker.getStore().clearFilter();
+	let taglist = PVE.Utils.tagList.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;
+	if (!me.addTag) {
+	    me.tag = tag;
+	    me.updateColor(me.tag);
+	}
+
+	if (update) {
+	    me.fireEvent('change', tag);
+	}
+
+	me.tagEl().contentEditable = false;
+	me.setMode('editable');
+    },
+
+    cancelEdit: function(list, event) {
+	let me = this;
+	if (me.mode === 'edit') {
+	    me.setText(me.tag);
+	    me.finishEdit(false);
+	}
+    },
+
+    setText: function(text) {
+	let me = this;
+	me.tagEl().innerHTML = text;
+	me.layoutCallback();
+    },
+
+    getTag: function() {
+	return this.tagEl().innerHTML;
+    },
+
+    setMode: function(mode) {
+	let me = this;
+	let icon = me.icons[me.addTag ? 'addTag' : mode];
+	let iconStyle = 'cursor: pointer;';
+	let text = me.tag;
+	let cursor = 'pointer';
+	switch (mode) {
+	    case 'normal':
+		iconStyle += 'display: none;';
+		break;
+	    case 'editable':
+		break;
+	    case 'edit':
+		me.tagEl().contentEditable = true;
+		text = '';
+		cursor = undefined;
+		break;
+	    default: return;
+	}
+
+	if (me.addTag) {
+	    me.setText(text);
+	    me.setStyle('cursor', cursor);
+	}
+
+	me.iconEl().classList = `fa fa-${icon}${me.faIconStyle}`;
+	me.iconEl().style = iconStyle;
+	me.mode = mode;
+    },
+
+    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;
+	let rgb = PVE.Utils.tagOverrides[tag] ?? Proxmox.Utils.stringToRGB(tag);
+
+	let cls = Proxmox.Utils.getTextContrastClass(rgb);
+	let color = Proxmox.Utils.rgbToCss(rgb);
+	me.setUserCls(`proxmox-tag-${cls}`);
+	me.setStyle('background-color', color);
+	if (rgb.length > 3) {
+	    let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]);
+	    me.setStyle('color', fgcolor);
+	} else {
+	    me.setStyle('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";
+	}
+	me.mode = me.mode ?? 'normal';
+
+	if (me.addTag) {
+	    me.tag = gettext('Add Tag');
+	    me.mode = 'editable';
+	    me.setUserCls(`proxmox-tag-dark`);
+	}
+
+	let iconStyle = me.mode !== 'editable' ? 'display: none' : '';
+	let iconCls = me.icons[me.addTag ? 'addTag' : me.mode];
+
+	let icon = ` <i style="cursor: pointer; ${iconStyle}" class="fa fa-${iconCls}${me.faIconStyle}"></i>`;
+	me.html = `<span style="padding-right: 1px">${me.tag}</span>${icon}`;
+
+	me.callParent();
+	if (me.addTag) {
+	    me.setStyle('cursor', 'pointer');
+	} else {
+	    me.updateColor(me.tag);
+	}
+    },
+});
-- 
2.30.2





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

* [pve-devel] [PATCH manager v5 08/11] ui: add form/TagEdit.js
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (10 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 07/11] ui: add form/Tag Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 09/11] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
                   ` (3 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 UTC (permalink / raw)
  To: pve-devel

this is a wrapper container for holding a list of (editable) tags
intended to be used in the lxc/qemu status toolbar

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

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 45862e71..0d43bde2 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -75,6 +75,7 @@ JSSRC= 							\
 	form/iScsiProviderSelector.js			\
 	form/TagColorGrid.js				\
 	form/Tag.js					\
+	form/TagEdit.js					\
 	grid/BackupView.js				\
 	grid/FirewallAliases.js				\
 	grid/FirewallOptions.js				\
diff --git a/www/manager6/form/TagEdit.js b/www/manager6/form/TagEdit.js
new file mode 100644
index 00000000..95879adc
--- /dev/null
+++ b/www/manager6/form/TagEdit.js
@@ -0,0 +1,131 @@
+Ext.define('PVE.panel.TagEditContainer', {
+    extend: 'Ext.container.Container',
+    alias: 'widget.pveTagEditContainer',
+
+    tagCount: 0,
+
+    layout: {
+	type: 'hbox',
+	align: 'stretch',
+    },
+
+    loadTags: function(tagstring = '') {
+	let me = this;
+
+	if (me.oldTags === tagstring) {
+	    return;
+	}
+
+	let tags = tagstring.split(/[;, ]/).filter((t) => !!t) || [];
+	me.suspendLayout = true;
+	me.tags = {};
+	me.removeAllTags();
+	tags.forEach((tag) => {
+	    me.addTag(tag);
+	});
+	me.suspendLayout = false;
+	me.updateLayout();
+	me.oldTags = tagstring;
+    },
+
+    getEditBtnHtml: function() {
+	let me = this;
+	let cls = '';
+	let qtip = '';
+	if (me.editMode) {
+	    qtip = gettext('Apply Changes');
+	    cls = 'check';
+	} else {
+	    qtip = gettext('Edit Tags');
+	    cls = 'pencil';
+	}
+	return `&nbsp;<i data-qtip="${qtip}" class="fa fa-${cls}"></i>`;
+    },
+
+    onEditClick: function() {
+	let me = this;
+	me.editMode = !me.editMode;
+	let tagCount = 0;
+	me.tagFields.forEach((tag) => {
+	    tag.setMode(me.editMode ? 'editable' : 'normal');
+	    if (tag.isVisible() && !tag.addTag) {
+		tagCount++;
+	    }
+	});
+
+	me.addTagBtn.setVisible(me.editMode);
+	me.editBtn.setHtml(me.getEditBtnHtml());
+	me.noTagsField.setVisible(!me.editMode && tagCount === 0);
+
+	if (!me.editMode) {
+	    let tags = [];
+	    me.tagFields.forEach((tag) => {
+		let tagValue = tag.getTag();
+		if (tag.isVisible() && tagValue) {
+		    tags.push(tagValue);
+		}
+	    });
+	    tags = tags.join(',');
+	    if (me.oldTags !== tags) {
+		me.oldTags = tags;
+		me.fireEvent('change', tags);
+	    }
+	}
+	me.updateLayout();
+    },
+
+    removeAllTags: function() {
+	let me = this;
+	me.tagFields.forEach((tag) => {
+	    me.remove(tag);
+	});
+	me.tagFieds = [];
+    },
+
+    addTag: function(tag, inEdit) {
+	let me = this;
+	let tagField = me.insert(me.tagFields.length + 1, {
+	    xtype: 'pmxTag',
+	    tag,
+	    mode: inEdit ? 'editable' : 'normal',
+	    layoutCallback: () => me.updateLayout(),
+	});
+	me.tagFields.push(tagField);
+	me.noTagsField.setVisible(false);
+    },
+
+    initComponent: function() {
+	let me = this;
+	me.tagFields = [];
+	me.callParent();
+	me.noTagsField = me.add({
+	    xtype: 'box',
+	    html: gettext('No Tags'),
+	});
+	me.addTagBtn = me.add({
+	    xtype: 'pmxTag',
+	    addTag: true,
+	    hidden: true,
+	    layoutCallback: () => me.updateLayout(),
+	    listeners: {
+		change: function(tag) {
+		    me.addTag(tag, true);
+		},
+	    },
+	});
+	me.editBtn = me.add({
+	    xtype: 'box',
+	    html: me.getEditBtnHtml(),
+	    style: {
+		cursor: 'pointer',
+	    },
+	    listeners: {
+		click: () => me.onEditClick(),
+		element: 'el',
+	    },
+	});
+	if (me.tags) {
+	    me.loadTags(me.tags);
+	}
+    },
+});
-- 
2.30.2





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

* [pve-devel] [PATCH manager v5 09/11] ui: {lxc, qemu}/Config: show Tags and make them editable
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (11 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 08/11] ui: add form/TagEdit.js Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 10/11] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
                   ` (2 subsequent siblings)
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 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  | 32 ++++++++++++++++++++++++++++++--
 www/manager6/qemu/Config.js | 31 +++++++++++++++++++++++++++++--
 2 files changed, 59 insertions(+), 4 deletions(-)

diff --git a/www/manager6/lxc/Config.js b/www/manager6/lxc/Config.js
index 89b59c9b..9f1994d3 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,33 @@ Ext.define('PVE.lxc.Config', {
 	    ],
 	});
 
+	let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
+	    tags: vm.tags,
+	    listeners: {
+		change: function(tags) {
+		    Proxmox.Utils.API2Request({
+			url: base_url + '/config',
+			method: 'PUT',
+			params: {
+			    tags,
+			},
+			success: function() {
+			    me.statusStore.load();
+			},
+			failure: function(response) {
+			    Ext.Msg.alert('Error', response.htmlStatus);
+			    me.statusStore.load();
+			},
+		    });
+		},
+	    },
+	});
 
 	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, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
 	    defaults: { statusStore: me.statusStore },
 	    items: [
 		{
@@ -344,10 +367,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 +382,9 @@ Ext.define('PVE.lxc.Config', {
 
 	    statusTxt.update({ lock: lock });
 
+	    rec = s.data.get('tags');
+	    tagsContainer.loadTags(rec?.data?.value);
+
 	    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..2cd6d856 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,33 @@ Ext.define('PVE.qemu.Config', {
 	    ],
 	});
 
+	let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
+	    tags: vm.tags,
+	    listeners: {
+		change: function(tags) {
+		    Proxmox.Utils.API2Request({
+			url: base_url + '/config',
+			method: 'PUT',
+			params: {
+			    tags,
+			},
+			success: function() {
+			    me.statusStore.load();
+			},
+			failure: function(response) {
+			    Ext.Msg.alert('Error', response.htmlStatus);
+			    me.statusStore.load();
+			},
+		    });
+		},
+	    },
+	});
+
 	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, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
 	    defaults: { statusStore: me.statusStore },
 	    items: [
 		{
@@ -382,11 +405,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 +423,9 @@ Ext.define('PVE.qemu.Config', {
 		xtermjs = !!s.data.get('serial');
 	    }
 
+	    rec = s.data.get('tags');
+	    tagsContainer.loadTags(rec?.data?.value);
+
 	    if (template) {
 		return;
 	    }
-- 
2.30.2





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

* [pve-devel] [PATCH manager v5 10/11] ui: tree/ResourceTree: show Tags in tree
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (12 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 09/11] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 11/11] ui: form/GlobalSearchField: display tags and allow to search for them Dominik Csapak
  2022-04-08 12:15 ` [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Matthias Heiserer
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 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 9f1994d3..1b79628e 100644
--- a/www/manager6/lxc/Config.js
+++ b/www/manager6/lxc/Config.js
@@ -206,8 +206,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, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js
index 2cd6d856..5c8fa620 100644
--- a/www/manager6/qemu/Config.js
+++ b/www/manager6/qemu/Config.js
@@ -242,8 +242,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, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
diff --git a/www/manager6/tree/ResourceTree.js b/www/manager6/tree/ResourceTree.js
index 139defab..d41721b9 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, PVE.Utils.tagOverrides);
+
 	info.text = status + info.text;
     },
 
-- 
2.30.2





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

* [pve-devel] [PATCH manager v5 11/11] ui: form/GlobalSearchField: display tags and allow to search for them
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (13 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 10/11] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
@ 2022-04-08  7:45 ` Dominik Csapak
  2022-04-08 12:15 ` [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Matthias Heiserer
  15 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2022-04-08  7:45 UTC (permalink / raw)
  To: pve-devel

each tag is treated like a seperate field, so it weighs more if the user
searches for the exact string of a single tag

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

diff --git a/www/manager6/form/GlobalSearchField.js b/www/manager6/form/GlobalSearchField.js
index 267a480d..8e815d4f 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,11 @@ Ext.define('PVE.form.GlobalSearchField', {
 		text: gettext('Description'),
 		flex: 1,
 		dataIndex: 'text',
+		renderer: function(value, mD, rec) {
+		    let overrides = PVE.Utils.tagOverrides;
+		    let tags = PVE.Utils.renderTags(rec.data.tags, overrides);
+		    return `${value}${tags}`;
+		},
 	    },
 	    {
 		text: gettext('Node'),
@@ -104,16 +110,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] 19+ messages in thread

* Re: [pve-devel] [PATCH cluster v5 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  2022-04-08  7:45 ` [pve-devel] [PATCH cluster v5 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-04-08 10:05   ` Matthias Heiserer
  0 siblings, 0 replies; 19+ messages in thread
From: Matthias Heiserer @ 2022-04-08 10:05 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

On 08.04.2022 09:45, Dominik Csapak wrote:
8<
> +// checks the conf for lines starting with the given props and
> +// writes the pointers into the correct positions into the 'found' array
> +// afterwards, whithout initial whitespace(s), we only deal with the format
whithout -> without




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

* Re: [pve-devel] [PATCH manager v5 05/11] ui: add form/TagColorGrid
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 05/11] ui: add form/TagColorGrid Dominik Csapak
@ 2022-04-08 11:39   ` Matthias Heiserer
  0 siblings, 0 replies; 19+ messages in thread
From: Matthias Heiserer @ 2022-04-08 11:39 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

On 08.04.2022 09:45, Dominik Csapak wrote:
8<
> +
> +    setValue: function(value) {
> +	let me = this;
> +	me.setColor(value);
> +	if (value && value.length === 6) {
> +	    me.picker.value = value[0] !== '#' ? `#${value}` : value;
> +	}
> +    },
> +
> +    setColor: function(value) {
> +	let me = this;
> +	let oldValue = me.realvalue;
> +	me.realvalue = value;
> +	let color = value.length === 6 ? `#${value}` : undefined;
> +	me.down('#picker').setStyle('background-color', color);Not a big fan of this initially being white. Makes it somewhat hard to 
discover.

8<
> +
> +Ext.define('PVE.form.TagColorGrid', {
> +    extend: 'Ext.grid.Panel',
> +    alias: 'widget.pveTagColorGrid',
> +
> +    mixins: [
> +	'Ext.form.field.Field',
> +    ],
> +

I don't like how the selection in the grid works. I.e. one has to click 
the sliver of whitespace between the columns to select a row, while 
writing doesn't change the selected column.

8<---
> +	},
> +    ],
> +
> +    columns: [
> +	{
> +	    header: 'Tag',
> +	    dataIndex: 'tag',
> +	    xtype: 'widgetcolumn',
> +	    onWidgetAttach: function(col, widget, rec) {
> +		widget.getStore().setData(PVE.Utils.tagList.map(v => ({ tag: v })));
> +	    },
> +	    widget: {
> +		xtype: 'combobox',
> +		isFormField: false,
> +		maskRe: /[a-zA-Z0-9_.-]/,
As it gets used several times in different locations (form/Tag), you 
could consider storing it in a fixed place.

8<---




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

* Re: [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui
  2022-04-08  7:45 [pve-devel] [PATCH cluster/widget-toolkit/manager v5] add tags to ui Dominik Csapak
                   ` (14 preceding siblings ...)
  2022-04-08  7:45 ` [pve-devel] [PATCH manager v5 11/11] ui: form/GlobalSearchField: display tags and allow to search for them Dominik Csapak
@ 2022-04-08 12:15 ` Matthias Heiserer
  15 siblings, 0 replies; 19+ messages in thread
From: Matthias Heiserer @ 2022-04-08 12:15 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

A few usability things:

* When adding tags in the guest view, one can add an empty tag that 
won't be saved, but will be displayed until view is reloaded. Should be 
discarded on creation.

* The hitbox of the ✓ is rather small

* Options/Tag colors could mention that it only manages the overrides

* (mentioned in mail to commit) In "Edit Tag colors", the default color 
should be something different from the background. E.g. red line from 
bottom left to top right, or use the autogenerated color


Tested-by: Matthias Heiserer <m.heiserer@proxmox.com>




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

end of thread, other threads:[~2022-04-08 12:16 UTC | newest]

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

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