public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui
@ 2022-10-18 14:02 Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
                   ` (21 more replies)
  0 siblings, 22 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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/resource grids
* 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 shapes available for the tree
* implements some permission control in datacenter.cfg with
  'user-tag-privileges' and 'admin-tags' with that it's possible to have
  better control over what tags a user can actually add to its guests
  i intentionally left out the gui for those for now, but they shouldn't
  be that hard to add, should we go this way

changes from v7:
* rebase on master
* changed admin tags from special syntax to datacenter.cfg option
* add 'user-tag-privleges' option and according api permission checks
* fixed some small bugs with permission checks (e.g. now we check
  the tag count, so that users cannot add tags multiple times that
  already exist but they have no privileges for)
* completely reworked the form/Tag and form/TagEdit, their
  implmementation is now much cleaner imho (squashed the drag&drop
  changes into the intial patches)
* squashed some patches together that fit together
* fixed some drag&drop bugs, so it should now work much better

changes from v6:
* reworded some commit messages
* added small benchmark result to CFS_IPC_GET_GUEST_CONFIG_PROPERTIES commit msg
* reshaped datacenter.cfg format into a property-string
  (also combined the gui edit window for shape+color override)
* refactored the pve-tags regex in pve-common/JSONSchema
* added admin tags ('+tag' syntax) with priv checks in qemu-sever/pve-container
  and subtle highlighting in the gui (bold-text)
* added tag rendering in resource grids
* add patches for drag&drop support when editing

pve-cluster:

Dominik Csapak (4):
  add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  Cluster: add get_guest_config_properties
  datacenter.cfg: add option for tag-style
  DataCenterConfig: add tag rights control to the datacenter config

 data/PVE/Cluster.pm          |  27 ++++++
 data/PVE/DataCenterConfig.pm | 130 +++++++++++++++++++++++++
 data/src/cfs-ipc-ops.h       |   2 +
 data/src/server.c            |  64 +++++++++++++
 data/src/status.c            | 177 ++++++++++++++++++++++++-----------
 data/src/status.h            |   3 +
 6 files changed, 347 insertions(+), 56 deletions(-)

qemu-server:

Dominik Csapak (1):
  api: update: improve tag privilege check

 PVE/API2/Qemu.pm | 51 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 50 insertions(+), 1 deletion(-)

pve-container:

Dominik Csapak (1):
  check_ct_modify_config_perm: improve tag privilege check

 src/PVE/LXC.pm | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

proxmox-widget-toolkit:

Dominik Csapak (2):
  add tag related helpers
  Toolkit: add override for Ext.dd.DragDropManager

 src/Toolkit.js       | 16 ++++++++
 src/Utils.js         | 88 ++++++++++++++++++++++++++++++++++++++++++++
 src/css/ext6-pmx.css | 45 ++++++++++++++++++++++
 3 files changed, 149 insertions(+)

pve-manager:

Dominik Csapak (12):
  api: /cluster/resources: add tags to returned properties
  api: add /ui-options api call
  ui: call '/ui-options' and save the result in PVE.UIOptions
  ui: parse and save tag color overrides from /ui-options
  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: add tags to ResourceGrid and GlobalSearchField

 PVE/API2.pm                            |  43 +++
 PVE/API2/Cluster.pm                    |   9 +-
 www/css/ext6-pve.css                   |   5 +
 www/manager6/Makefile                  |   3 +
 www/manager6/Utils.js                  |  71 ++++-
 www/manager6/Workspace.js              |  22 ++
 www/manager6/data/ResourceStore.js     |   7 +
 www/manager6/dc/OptionView.js          |  88 +++++-
 www/manager6/form/GlobalSearchField.js |  20 +-
 www/manager6/form/Tag.js               | 233 ++++++++++++++++
 www/manager6/form/TagColorGrid.js      | 357 +++++++++++++++++++++++++
 www/manager6/form/TagEdit.js           | 316 ++++++++++++++++++++++
 www/manager6/grid/ResourceGrid.js      |   1 +
 www/manager6/lxc/Config.js             |  36 ++-
 www/manager6/qemu/Config.js            |  35 ++-
 www/manager6/tree/ResourceTree.js      |  20 +-
 16 files changed, 1247 insertions(+), 19 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] 26+ messages in thread

* [pve-devel] [PATCH cluster v8 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 2/4] Cluster: add get_guest_config_properties Dominik Csapak
                   ` (20 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

for getting multiple properties from the in memory config of the
guests in one go. Keep the existing IPC call as is for backward
compatibility and add this as separate, new one.

It basically behaves the same as
CFS_IPC_GET_GUEST_CONFIG_PROPERTY, but takes a list of properties
instead and returns multiple properties per guest.

The old way of getting a single property is now also done by
the new function, since it basically performs identically.

Here a short benchmark:

Setup:
PVE in a VM with cpu type host (12700k) and 4 cores
10000 typical configs with both 'lock' and 'tags' set at the end
and fairly long tags ('test-tag1,test-tag2,test-tag3')
(normal vm with a snapshot, ~1KiB)

i let it run 100 times each, times in ms

old code:

total time  time per iteration
1054.2      10.2

new code:

num props  total time  time per iter  function
2          1332.2      13.2           get_properties
1          1051.2      10.2           get_properties
2          2082.2      20.2           get_property (2 calls to get_property)
1          1034.2      10.2           get_property

so a call with the new code for one property is the same as with the
old code, and adding a second property only adds a bit of additional
time (in this case ~30%)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 data/src/cfs-ipc-ops.h |   2 +
 data/src/server.c      |  64 +++++++++++++++
 data/src/status.c      | 177 ++++++++++++++++++++++++++++-------------
 data/src/status.h      |   3 +
 4 files changed, 190 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..ced9cfc 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,63 @@ 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;
+
+		size_t remaining = 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 (rh->num_props == 0) {
+			cfs_debug("num_props == 0");
+			result = -EINVAL;
+		} else if (remaining <= 1) {
+			cfs_debug("property length <= 1, %ld", remaining);
+			result = -EINVAL;
+		} else {
+			const char **properties = malloc(sizeof(char*) * rh->num_props);
+			char *current = (rh->props);
+			for (uint8_t i = 0; i < rh->num_props; i++) {
+			    size_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 %u 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..e2bedaa 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, without 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,32 @@ _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) {
+			return; // found all
 		}
 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; // valid property lines end with \n, but none in the config
 		}
 		*next_newline = '\0';
 	}
 
-	return NULL; // not found
+	return;
 }
 
 static void
@@ -883,24 +901,77 @@ _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) {
+			continue;
+		}
+		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 +990,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 +1007,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 +1032,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] 26+ messages in thread

* [pve-devel] [PATCH cluster v8 2/4] Cluster: add get_guest_config_properties
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 3/4] datacenter.cfg: add option for tag-style Dominik Csapak
                   ` (19 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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 abcc46d..99e7975 100644
--- a/data/PVE/Cluster.pm
+++ b/data/PVE/Cluster.pm
@@ -339,10 +339,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] 26+ messages in thread

* [pve-devel] [PATCH cluster v8 3/4] datacenter.cfg: add option for tag-style
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 2/4] Cluster: add get_guest_config_properties Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-11-10  9:54   ` Thomas Lamprecht
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 4/4] DataCenterConfig: add tag rights control to the datacenter config Dominik Csapak
                   ` (18 subsequent siblings)
  21 siblings, 1 reply; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

its a property string containing 'tree-shape' and 'colors'
the colors are formatted like this:
<tag>:<background-color>[:<text-color>]

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

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 6a2adee..bb29d26 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -131,6 +131,29 @@ sub pve_verify_mac_prefix {
     return $mac_prefix;
 }
 
+my $COLOR_RE = '[0-9a-fA-F]{6}';
+my $TAG_COLOR_OVERRIDE_RE = "(?:${PVE::JSONSchema::PVE_TAG_RE}:${COLOR_RE}(?:\:${COLOR_RE})?)";
+
+my $tag_style_format = {
+    'tree-shape' => {
+	optional => 1,
+	type => 'string',
+	enum => ['full', 'circle', 'dense', 'none'],
+	default => 'circle',
+	description => "Tag style in tree. 'full' draws the full tag. 'circle' ".
+	    "draws only a circle with the background color. 'dense' only draws ".
+	    "a small rectancle (useful when many tags are assigned to each guest).".
+	    "'none' disables showing the tags.",
+    },
+    'colors' => {
+	optional => 1,
+	type => 'string',
+	pattern => "${TAG_COLOR_OVERRIDE_RE}(?:\;$TAG_COLOR_OVERRIDE_RE)*",
+	typetext => '<tag>:<hex-color>[:<hex-color-for-text>][;<tag>=...]',
+	description => "Manual color mapping for tags (semicolon separated).",
+    },
+};
+
 my $datacenter_schema = {
     type => "object",
     additionalProperties => 0,
@@ -256,6 +279,12 @@ my $datacenter_schema = {
 	    maxLength => 64 * 1024,
 	    optional => 1,
 	},
+	'tag-style' => {
+	    optional => 1,
+	    type => 'string',
+	    description => "Tag style options.",
+	    format => $tag_style_format,
+	},
     },
 };
 
@@ -300,6 +329,10 @@ sub parse_datacenter_config {
 	$res->{webauthn} = parse_property_string($webauthn_format, $webauthn);
     }
 
+    if (my $tag_style = $res->{'tag-style'}) {
+	$res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
+    }
+
     # for backwards compatibility only, new migration property has precedence
     if (defined($res->{migration_unsecure})) {
 	if (defined($res->{migration}->{type})) {
@@ -359,6 +392,10 @@ sub write_datacenter_config {
 	$cfg->{webauthn} = PVE::JSONSchema::print_property_string($webauthn, $webauthn_format);
     }
 
+    if (ref(my $tag_style = $cfg->{'tag-style'})) {
+	$cfg->{'tag-style'} = PVE::JSONSchema::print_property_string($tag_style, $tag_style_format);
+    }
+
     my $comment = '';
     # add description as comment to top of file
     my $description = $cfg->{description} || '';
-- 
2.30.2





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

* [pve-devel] [PATCH cluster v8 4/4] DataCenterConfig: add tag rights control to the datacenter config
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (2 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 3/4] datacenter.cfg: add option for tag-style Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-11-09 14:42   ` Aaron Lauterer
  2022-11-10 10:09   ` Thomas Lamprecht
  2022-10-18 14:02 ` [pve-devel] [PATCH qemu-server v8] api: update: improve tag privilege check Dominik Csapak
                   ` (17 subsequent siblings)
  21 siblings, 2 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

by adding a 'user-tag-privileges' and 'admin-tags' option.
The first sets the policy by which "normal" users (with
'VM.Config.Options' on the respective guest) can create/delete tags
and the second is a list of tags only settable by 'admins'
('Sys.Modify' on '/')

also add a helper 'get_user_admin_tags' that returns two hashmaps that
determines the allowed user tags and admin tags that require elevated
permissions

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

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index bb29d26..e2140ff 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -154,6 +154,26 @@ my $tag_style_format = {
     },
 };
 
+my $user_tag_privs_format = {
+    'usable' => {
+	optional => 1,
+	type => 'string',
+	enum => ['none', 'list', 'existing', 'free'],
+	default => 'free',
+	dscription => "Determines which tags a user without Sys.Modify on '/' can set and delete. ".
+	    "'none' means no tags are settable.'list' allows tags from the given list. ".
+	    "'existing' means only already existing tags or from the given list. ".
+	    "And 'free' means users can assign any tags."
+    },
+    'list' => {
+	optional => 1,
+	type => 'string',
+	pattern => "${PVE::JSONSchema::PVE_TAG_RE}(?:\;${PVE::JSONSchema::PVE_TAG_RE})*",
+	typetext => "<tag>[;<tag>=...]",
+	description => "List of tags users are allowd to set and delete (semicolon separated).",
+    },
+};
+
 my $datacenter_schema = {
     type => "object",
     additionalProperties => 0,
@@ -285,12 +305,60 @@ my $datacenter_schema = {
 	    description => "Tag style options.",
 	    format => $tag_style_format,
 	},
+	'user-tag-privileges' => {
+	    optional => 1,
+	    type => 'string',
+	    description => "Privilege options for user settable tags",
+	    format => $user_tag_privs_format,
+	},
+	'admin-tags' => {
+	    optional => 1,
+	    type => 'string',
+	    description => "A list of tags only admins (Sys.Modify on '/') are allowed to set/delete",
+	    pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
+	},
     },
 };
 
 # make schema accessible from outside (for documentation)
 sub get_datacenter_schema { return $datacenter_schema };
 
+# returns two hashmaps of tags, the first is the list of tags that can
+# be used by users with 'VM.Config.Options', and the second is a list
+# that needs 'Sys.Modify' on '/'
+#
+# If the first map is 'undef', it means there is generally no restriction
+# besides the tags defined in the second map.
+#
+# CAUTION: this function may include tags from *all* guest configs,
+# regardless of the current authuser
+sub get_user_admin_tags {
+    my $user_tags = {};
+    my $admin_tags = {};
+
+    my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
+    if (my $user_tag_privs = $dc->{'user-tag-privileges'}) {
+	my $usable = $user_tag_privs->{usable} // 'free';
+	if ($usable eq 'free') {
+	    $user_tags = undef;
+	} elsif ($usable eq 'existing') {
+	    map { $user_tags->{$_} = 1 } ($user_tag_privs->{list} // [])->@*;
+	    my $props = PVE::Cluster::get_guest_config_properties(['tags']);
+	    for my $vmid (keys $props->%*) {
+		map { $user_tags->{$_} = 1 } PVE::Tools::split_list($props->{$vmid}->{tags});
+	    }
+	} elsif ($usable eq 'list') {
+	    map { $user_tags->{$_} = 1 } ($user_tag_privs->{list} // [])->@*;
+	}
+    }
+    if (my $tags = $dc->{'admin-tags'}) {
+	$admin_tags = {};
+	map { $admin_tags->{$_} = 1 } $tags->@*;
+    }
+
+    return ($user_tags, $admin_tags);
+}
+
 sub parse_datacenter_config {
     my ($filename, $raw) = @_;
 
@@ -333,6 +401,19 @@ sub parse_datacenter_config {
 	$res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
     }
 
+    if (my $user_tag_privs = $res->{'user-tag-privileges'}) {
+	$res->{'user-tag-privileges'} =
+	    parse_property_string($user_tag_privs_format, $user_tag_privs);
+
+	    if (my $user_tags = $res->{'user-tag-privileges'}->{list}) {
+	        $res->{'user-tag-privileges'}->{list} = [split(';', $user_tags)];
+	    }
+    }
+
+    if (my $admin_tags = $res->{'admin-tags'}) {
+	$res->{'admin-tags'} = [split(';', $admin_tags)];
+    }
+
     # for backwards compatibility only, new migration property has precedence
     if (defined($res->{migration_unsecure})) {
 	if (defined($res->{migration}->{type})) {
@@ -396,6 +477,18 @@ sub write_datacenter_config {
 	$cfg->{'tag-style'} = PVE::JSONSchema::print_property_string($tag_style, $tag_style_format);
     }
 
+    if (ref(my $user_tag_privs = $cfg->{'user-tag-privileges'})) {
+	if (my $user_tags = $user_tag_privs->{list}) {
+	    $user_tag_privs->{list} = join(';', sort ($user_tags));
+	}
+	$cfg->{'user-tag-privileges'} =
+	    PVE::JSONSchema::print_property_string($user_tag_privs, $user_tag_privs_format);
+    }
+
+    if (ref(my $admin_tags = $cfg->{'admin-tags'})) {
+	$cfg->{'admin-tags'} = join(';', sort $admin_tags->@*);
+    }
+
     my $comment = '';
     # add description as comment to top of file
     my $description = $cfg->{description} || '';
-- 
2.30.2





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

* [pve-devel] [PATCH qemu-server v8] api: update: improve tag privilege check
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (3 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 4/4] DataCenterConfig: add tag rights control to the datacenter config Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH container v8] check_ct_modify_config_perm: " Dominik Csapak
                   ` (16 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

'normal' tags require 'VM.Config.Options' on '/vm/<vmid>', but not
allowed tags (either limited with 'user-tag-privileges' or 'admin-tags'
in the datacenter config) require 'Sys.Modify' on '/'

this patch implements the proper checks on adding/editing/deleting
these permissions

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

diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
index 3ec31c2..27fbdcc 100644
--- a/PVE/API2/Qemu.pm
+++ b/PVE/API2/Qemu.pm
@@ -538,7 +538,6 @@ my $generaloptions = {
     'startup' => 1,
     'tdf' => 1,
     'template' => 1,
-    'tags' => 1,
 };
 
 my $vmpoweroptions = {
@@ -608,6 +607,7 @@ my $check_vm_modify_config_perm = sub {
 	next if PVE::QemuServer::is_valid_drivename($opt);
 	next if $opt eq 'cdrom';
 	next if $opt =~ m/^(?:unused|serial|usb)\d+$/;
+	next if $opt eq 'tags';
 
 
 	if ($cpuoptions->{$opt} || $opt =~ m/^numa\d+$/) {
@@ -1520,6 +1520,29 @@ my $update_vm_api  = sub {
 		}
 	    };
 
+	    my $user_tags;
+	    my $admin_tags;
+
+	    my $check_tag_perms = sub {
+		my ($tag) = @_;
+
+		if ($rpcenv->check($authuser, '/', ['Sys.Modify'], 1)) {
+		    return;
+		}
+
+		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options']);
+
+		if (!defined($user_tags) && !defined($admin_tags)) {
+		    ($user_tags, $admin_tags) = PVE::DataCenterConfig::get_user_admin_tags();
+		}
+
+		if ((defined($user_tags) && !$user_tags->{$tag}) ||
+		    (defined($admin_tags) && $admin_tags->{$tag})
+		) {
+		    $rpcenv->check($authuser, '/', ['Sys.Modify']);
+		}
+	    };
+
 	    foreach my $opt (@delete) {
 		$modified->{$opt} = 1;
 		$conf = PVE::QemuConfig->load_config($vmid); # update/reload
@@ -1578,6 +1601,13 @@ my $update_vm_api  = sub {
 		    }
 		    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
 		    PVE::QemuConfig->write_config($vmid, $conf);
+		} elsif ($opt eq 'tags') {
+		    foreach my $tag (PVE::Tools::split_list($val)) {
+			check_tag_perms->($tag);
+		    }
+
+		    delete $conf->{$opt};
+		    PVE::QemuConfig->write_config($vmid, $conf);
 		} else {
 		    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
 		    PVE::QemuConfig->write_config($vmid, $conf);
@@ -1637,6 +1667,25 @@ my $update_vm_api  = sub {
 		    } elsif ($authuser ne 'root@pam') {
 			die "only root can modify '$opt' config for real devices\n";
 		    }
+		    $conf->{pending}->{$opt} = $param->{$opt};
+		} elsif ($opt eq 'tags') {
+		    my $old_tags = {};
+		    my $new_tags = {};
+
+		    map { $old_tags->{$_} += 1 } PVE::Tools::split_list($conf->{$opt} // '');
+		    map { $new_tags->{$_} += 1 } PVE::Tools::split_list($param->{$opt});
+
+		    my $check_tags = sub {
+			my ($a, $b) = @_;
+			foreach my $tag (keys %$a) {
+			    next if ($b->{$tag} // 0) == ($a->{$tag} // 0);
+			    $check_tag_perms->($tag);
+			}
+		    };
+
+		    $check_tags->($old_tags, $new_tags);
+		    $check_tags->($new_tags, $old_tags);
+
 		    $conf->{pending}->{$opt} = $param->{$opt};
 		} else {
 		    $conf->{pending}->{$opt} = $param->{$opt};
-- 
2.30.2





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

* [pve-devel] [PATCH container v8] check_ct_modify_config_perm: improve tag privilege check
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (4 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH qemu-server v8] api: update: improve tag privilege check Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH widget-toolkit v8 1/2] add tag related helpers Dominik Csapak
                   ` (15 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

'normal' tags require 'VM.Config.Options' on '/vm/<vmid>', but not
allowed tags (either limited with 'user-tag-privileges' or 'admin-tags'
in the datacenter config) require 'Sys.Modify' on '/'

this patch implements the proper checks on adding/editing/deleting
these permissions

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

diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index fe63087..c31c95c 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -1333,6 +1333,45 @@ sub check_ct_modify_config_perm {
 	} elsif ($opt eq 'hookscript') {
 	    # For now this is restricted to root@pam
 	    raise_perm_exc("changing the hookscript is only allowed for root\@pam");
+	} elsif ($opt eq 'tags') {
+	    my $user_tags;
+	    my $admin_tags;
+
+	    my $check_tag_perms = sub {
+		my ($tag) = @_;
+		if ($rpcenv->check($authuser, '/', ['Sys.Modify'], 1)) {
+		    return;
+		}
+
+		$rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options']);
+
+		if (!defined($user_tags) && !defined($admin_tags)) {
+		    ($user_tags, $admin_tags) = PVE::DataCenterConfig::get_user_admin_tags();
+		}
+
+		if ((defined($user_tags) && !$user_tags->{$tag}) ||
+		    (defined($admin_tags) && $admin_tags->{$tag})
+		) {
+		    $rpcenv->check($authuser, '/', ['Sys.Modify']);
+		}
+	    };
+
+	    my $old_tags = {};
+	    my $new_tags = {};
+
+	    map { $old_tags->{$_} += 1 } PVE::Tools::split_list($oldconf->{$opt} // '');
+	    map { $new_tags->{$_} += 1 } PVE::Tools::split_list($newconf->{$opt});
+
+	    my $check_tags = sub {
+		my ($a, $b) = @_;
+		foreach my $tag (keys %$a) {
+		    next if ($b->{$tag} // 0) == ($a->{$tag} // 0);
+		    $check_tag_perms->($tag);
+		}
+	    };
+
+	    $check_tags->($old_tags, $new_tags);
+	    $check_tags->($new_tags, $old_tags);
 	} else {
 	    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
 	}
-- 
2.30.2





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

* [pve-devel] [PATCH widget-toolkit v8 1/2] add tag related helpers
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (5 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH container v8] check_ct_modify_config_perm: " Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH widget-toolkit v8 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
                   ` (14 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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         | 88 ++++++++++++++++++++++++++++++++++++++++++++
 src/css/ext6-pmx.css | 45 ++++++++++++++++++++++
 2 files changed, 133 insertions(+)

diff --git a/src/Utils.js b/src/Utils.js
index 6a03057..f491fd1 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1272,6 +1272,94 @@ 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]})`;
+    },
+
+    rgbToHex: function(rgb) {
+	let r = Math.round(rgb[0]).toString(16);
+	let g = Math.round(rgb[1]).toString(16);
+	let b = Math.round(rgb[2]).toString(16);
+	return `${r}${g}${b}`;
+    },
+
+    hexToRGB: function(hex) {
+	if (!hex) {
+	    return undefined;
+	}
+	if (hex.length === 7) {
+	    hex = hex.slice(1);
+	}
+	let r = parseInt(hex.slice(0, 2), 16);
+	let g = parseInt(hex.slice(2, 4), 16);
+	let b = parseInt(hex.slice(4, 6), 16);
+	return [r, g, b];
+    },
+
+    // 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 style = `background-color: ${Proxmox.Utils.rgbToCss(rgb)};`;
+	let cls;
+	if (rgb.length > 3) {
+	    style += `color: ${Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]])}`;
+	    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 231b4ce..bb85ffb 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] 26+ messages in thread

* [pve-devel] [PATCH widget-toolkit v8 2/2] Toolkit: add override for Ext.dd.DragDropManager
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (6 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH widget-toolkit v8 1/2] add tag related helpers Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 01/12] api: /cluster/resources: add tags to returned properties Dominik Csapak
                   ` (13 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

to fix selection behavior for Ext.dd.DragZone.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/Toolkit.js | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/src/Toolkit.js b/src/Toolkit.js
index c730374..20b6eba 100644
--- a/src/Toolkit.js
+++ b/src/Toolkit.js
@@ -681,6 +681,22 @@ Ext.define('Proxmox.view.DragZone', {
     },
 });
 
+// Fix text selection on drag when using DragZone,
+// see https://forum.sencha.com/forum/showthread.php?335100
+Ext.define('Proxmox.dd.DragDropManager', {
+    override: 'Ext.dd.DragDropManager',
+
+    stopEvent: function(e) {
+	if (this.stopPropagation) {
+	    e.stopPropagation();
+	}
+
+	if (this.preventDefault) {
+	    e.preventDefault();
+	}
+    },
+});
+
 // force alert boxes to be rendered with an Error Icon
 // since Ext.Msg is an object and not a prototype, we need to override it
 // after the framework has been initiated
-- 
2.30.2





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

* [pve-devel] [PATCH manager v8 01/12] api: /cluster/resources: add tags to returned properties
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (7 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH widget-toolkit v8 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 02/12] api: add /ui-options api call Dominik Csapak
                   ` (12 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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 d6b405e2..bc327e76 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] 26+ messages in thread

* [pve-devel] [PATCH manager v8 02/12] api: add /ui-options api call
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (8 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 01/12] api: /cluster/resources: add tags to returned properties Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
                   ` (11 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

which contains ui relevant options, like the console preference and tag-style

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

diff --git a/PVE/API2.pm b/PVE/API2.pm
index a4256160..2acdecdb 100644
--- a/PVE/API2.pm
+++ b/PVE/API2.pm
@@ -118,6 +118,7 @@ __PACKAGE__->register_method ({
 
 	my $res = {};
 
+	# TODO remove with next major release
 	my $datacenter_confg = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
 	for my $k (qw(console)) {
 	    $res->{$k} = $datacenter_confg->{$k} if exists $datacenter_confg->{$k};
@@ -130,4 +131,46 @@ __PACKAGE__->register_method ({
 	return $res;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'ui-options',
+    path => 'ui-options',
+    method => 'GET',
+    permissions => { user => 'all' },
+    description => "Global options regarding the UI.",
+    parameters => {
+	additionalProperties => 0,
+	properties => {},
+    },
+    returns => {
+	type => "object",
+	properties => {
+	    console => {
+		type => 'string',
+		enum => ['applet', 'vv', 'html5', 'xtermjs'],
+		optional => 1,
+		description => 'The default console viewer to use.',
+	    },
+	    'tag-style' => {
+		type => 'string',
+		optional => 1,
+		description => 'Cluster wide tag style overrides',
+	    },
+	},
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $res = {};
+
+	my $rpcenv = PVE::RPCEnvironment::get();
+	my $authuser = $rpcenv->get_user();
+
+	my $datacenter_confg = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
+	for my $k (qw(console tag-style)) {
+	    $res->{$k} = $datacenter_confg->{$k} if exists $datacenter_confg->{$k};
+	}
+
+	return $res;
+    }});
+
 1;
-- 
2.30.2





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

* [pve-devel] [PATCH manager v8 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (9 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 02/12] api: add /ui-options api call Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 04/12] ui: parse and save tag color overrides from /ui-options Dominik Csapak
                   ` (10 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

and move the use of the console from VersionInfo to here, since
this will be the future place for ui related backend options.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/Utils.js         |  2 +-
 www/manager6/Workspace.js     | 13 +++++++++++++
 www/manager6/dc/OptionView.js |  4 ++--
 3 files changed, 16 insertions(+), 3 deletions(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 7ca6a271..3c8c1417 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1332,7 +1332,7 @@ Ext.define('PVE.Utils', {
 	    allowSpice = consoles.spice;
 	    allowXtermjs = !!consoles.xtermjs;
 	}
-	let dv = PVE.VersionInfo.console || (type === 'kvm' ? 'vv' : 'xtermjs');
+	let dv = PVE.UIOptions.console || (type === 'kvm' ? 'vv' : 'xtermjs');
 	if (dv === 'vv' && !allowSpice) {
 	    dv = allowXtermjs ? 'xtermjs' : 'html5';
 	} else if (dv === 'xtermjs' && !allowXtermjs) {
diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js
index 2bb502e0..7e17964e 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -158,6 +158,14 @@ Ext.define('PVE.StdWorkspace', {
 		},
 	    });
 
+	    Proxmox.Utils.API2Request({
+		url: '/ui-options',
+		method: 'GET',
+		success: function(response) {
+		    me.updateUIOptions(response.result.data);
+		},
+	    });
+
 	    Proxmox.Utils.API2Request({
 		url: '/cluster/sdn',
 		method: 'GET',
@@ -213,6 +221,11 @@ Ext.define('PVE.StdWorkspace', {
 	ui.updateLayout();
     },
 
+    updateUIOptions: function(data) {
+	let me = this;
+	PVE.UIOptions = data ?? {};
+    },
+
     initComponent: function() {
 	let me = this;
 
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index 5a2be182..ff96351d 100644
--- a/www/manager6/dc/OptionView.js
+++ b/www/manager6/dc/OptionView.js
@@ -343,9 +343,9 @@ Ext.define('PVE.dc.OptionView', {
 	    }
 
 	    var rec = store.getById('console');
-	    PVE.VersionInfo.console = rec.data.value;
+	    PVE.UIOptions.console = rec.data.value;
 	    if (rec.data.value === '__default__') {
-		delete PVE.VersionInfo.console;
+		delete PVE.UIOptions.console;
 	    }
 	});
 
-- 
2.30.2





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

* [pve-devel] [PATCH manager v8 04/12] ui: parse and save tag color overrides from /ui-options
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (10 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 05/12] ui: tree/ResourceTree: collect tags on update Dominik Csapak
                   ` (9 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

into a global list of overrides. on update, also parse the values
from the browser localstore. Also emits a GlobalEvent 'loadedUiOptions'
so that e.g. the tags can listen to that and refresh their colors

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

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 3c8c1417..e32c679a 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1803,6 +1803,46 @@ Ext.define('PVE.Utils', {
     },
 
     notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],
+
+    parseTagOverrides: function(overrides) {
+	let colors = {};
+	(overrides || "").split(';').forEach(color => {
+	    if (!color) {
+		return;
+	    }
+	    let [tag, color_hex, font_hex] = color.split(':');
+	    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 (font_hex) {
+		colors[tag].push(parseInt(font_hex.slice(0, 2), 16));
+		colors[tag].push(parseInt(font_hex.slice(2, 4), 16));
+		colors[tag].push(parseInt(font_hex.slice(4, 6), 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);
+	Ext.GlobalEvents.fireEvent('tag-color-override');
+    },
+
+    updateTagSettings: function(overrides, style) {
+	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 7e17964e..616f127f 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -224,6 +224,15 @@ Ext.define('PVE.StdWorkspace', {
     updateUIOptions: function(data) {
 	let me = this;
 	PVE.UIOptions = data ?? {};
+	let colors = data?.['tag-style']?.colors;
+	let shape = data?.['tag-style']?.['tree-shape'];
+
+	PVE.Utils.updateTagSettings(colors, shape);
+	if (colors) {
+	    // refresh tree once
+	    PVE.data.ResourceStore.fireEvent('load');
+	    Ext.GlobalEvents.fireEvent('loadedUiOptions');
+	}
     },
 
     initComponent: function() {
-- 
2.30.2





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

* [pve-devel] [PATCH manager v8 05/12] ui: tree/ResourceTree: collect tags on update
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (11 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 04/12] ui: parse and save tag color overrides from /ui-options Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 06/12] ui: add form/TagColorGrid Dominik Csapak
                   ` (8 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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 e32c679a..bc808c68 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1804,6 +1804,13 @@ Ext.define('PVE.Utils', {
 
     notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],
 
+    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] 26+ messages in thread

* [pve-devel] [PATCH manager v8 06/12] ui: add form/TagColorGrid
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (12 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 05/12] ui: tree/ResourceTree: collect tags on update Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 07/12] ui: dc/OptionView: add editors for tag settings Dominik Csapak
                   ` (7 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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/Utils.js             |   2 +
 www/manager6/form/TagColorGrid.js | 357 ++++++++++++++++++++++++++++++
 4 files changed, 365 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 d16770b1..60ae421e 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/Utils.js b/www/manager6/Utils.js
index bc808c68..ba276ebe 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1850,6 +1850,8 @@ Ext.define('PVE.Utils', {
 
 	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
     },
+
+    tagCharRegex: /^[a-z0-9+_.-]$/i,
 },
 
     singleton: true,
diff --git a/www/manager6/form/TagColorGrid.js b/www/manager6/form/TagColorGrid.js
new file mode 100644
index 00000000..3ad8e07f
--- /dev/null
+++ b/www/manager6/form/TagColorGrid.js
@@ -0,0 +1,357 @@
+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;
+	let colors;
+	if (Ext.isObject(value)) {
+	    colors = value.colors;
+	} else {
+	    colors = value;
+	}
+	if (!colors) {
+	    me.getStore().removeAll();
+	    me.checkChange();
+	    return me;
+	}
+	let entries = (colors.split(';') || []).map((entry) => {
+	    let [tag, bg, fg] = entry.split(':');
+	    fg = fg || "";
+	    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();
+	},
+
+	tagChange: function(field, newValue, oldValue) {
+	    let me = this;
+	    let rec = field.getWidgetRecord();
+	    if (!rec) {
+		return;
+	    }
+	    if (newValue && newValue !== oldValue) {
+		let newrgb = Proxmox.Utils.stringToRGB(newValue);
+		let newvalue = Proxmox.Utils.rgbToHex(newrgb);
+		if (!rec.get('color')) {
+		    rec.set('color', newvalue);
+		} else if (oldValue) {
+		    let oldrgb = Proxmox.Utils.stringToRGB(oldValue);
+		    let oldvalue = Proxmox.Utils.rgbToHex(oldrgb);
+		    if (rec.get('color') === oldvalue) {
+			rec.set('color', newvalue);
+		    }
+		}
+	    }
+	    me.fieldChange(field, newValue, oldValue);
+	},
+
+	backgroundChange: function(field, newValue, oldValue) {
+	    let me = this;
+	    let rec = field.getWidgetRecord();
+	    if (!rec) {
+		return;
+	    }
+	    if (newValue && newValue !== oldValue) {
+		let newrgb = Proxmox.Utils.hexToRGB(newValue);
+		let newcls = Proxmox.Utils.getTextContrastClass(newrgb);
+		let hexvalue = newcls === 'dark' ? '000000' : 'FFFFFF';
+		if (!rec.get('text')) {
+		    rec.set('text', hexvalue);
+		} else if (oldValue) {
+		    let oldrgb = Proxmox.Utils.hexToRGB(oldValue);
+		    let oldcls = Proxmox.Utils.getTextContrastClass(oldrgb);
+		    let oldvalue = oldcls === 'dark' ? '000000' : 'FFFFFF';
+		    if (rec.get('text') === oldvalue) {
+			rec.set('text', hexvalue);
+		    }
+		}
+	    }
+	    me.fieldChange(field, newValue, oldValue);
+	},
+
+	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: PVE.Utils.tagCharRegex,
+		allowBlank: false,
+		queryMode: 'local',
+		displayField: 'tag',
+		valueField: 'tag',
+		store: {},
+		listeners: {
+		    change: 'tagChange',
+		},
+	    },
+	    flex: 1,
+	},
+	{
+	    header: gettext('Background'),
+	    xtype: 'widgetcolumn',
+	    flex: 1,
+	    dataIndex: 'color',
+	    widget: {
+		xtype: 'pveColorPicker',
+		isFormField: false,
+		listeners: {
+		    change: 'backgroundChange',
+		},
+	    },
+	},
+	{
+	    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] 26+ messages in thread

* [pve-devel] [PATCH manager v8 07/12] ui: dc/OptionView: add editors for tag settings
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (13 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 06/12] ui: add form/TagColorGrid Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 08/12] ui: add form/Tag Dominik Csapak
                   ` (6 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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 | 84 +++++++++++++++++++++++++++++++++++
 2 files changed, 104 insertions(+)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index ba276ebe..c2a98523 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1851,6 +1851,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;
+    },
+
     tagCharRegex: /^[a-z0-9+_.-]$/i,
 },
 
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index ff96351d..cb1b1dc7 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;
@@ -312,6 +313,86 @@ Ext.define('PVE.dc.OptionView', {
 		submitValue: true,
 	    }],
 	});
+	me.rows['tag-style'] = {
+	    required: true,
+	    renderer: (value) => {
+		if (value === undefined) {
+		    return gettext('No Overrides');
+		}
+		let colors = PVE.Utils.parseTagOverrides(value.colors);
+		let shape = value['tree-shape'];
+		let shapeText = PVE.Utils.tagTreeStyles[shape] ?? Proxmox.Utils.defaultText;
+		let txt = Ext.String.format(gettext("Tree Shape: {0}"), shapeText);
+		if (Object.keys(colors).length > 0) {
+		    txt += ', ';
+		}
+		for (const tag of Object.keys(colors)) {
+		    txt += Proxmox.Utils.getTagElement(tag, colors);
+		}
+		return txt;
+	    },
+	    header: gettext('Tag Style Override'),
+	    editor: {
+		xtype: 'proxmoxWindowEdit',
+		width: 800,
+		subject: gettext('Tag Color Override'),
+		fieldDefaults: {
+		    labelWidth: 100,
+		},
+		url: '/api2/extjs/cluster/options',
+		items: [
+		    {
+			xtype: 'inputpanel',
+			setValues: function(values) {
+			    if (values === undefined) {
+				return undefined;
+			    }
+			    values = values?.['tag-style'] ?? {};
+			    values['tree-shape'] = values?.['tree-shape'] || '__default__';
+			    return Proxmox.panel.InputPanel.prototype.setValues.call(this, values);
+			},
+			onGetValues: function(values) {
+			    let style = {};
+			    if (values.colors) {
+				style.colors = values.colors;
+			    }
+			    if (values['tree-shape']) {
+				style['tree-shape'] = values['tree-shape'];
+			    }
+			    let value = PVE.Parser.printPropertyString(style);
+			    if (value === '') {
+				return {
+				    'delete': 'tag-style',
+				};
+			    }
+			    return {
+				'tag-style': value,
+			    };
+			},
+			items: [
+			    {
+				name: 'tree-shape',
+				xtype: 'proxmoxKVComboBox',
+				fieldLabel: gettext('Tree Shape'),
+				comboItems: Object.entries(PVE.Utils.tagTreeStyles),
+				defaultValue: '__default__',
+				deleteEmpty: true,
+			    },
+			    {
+				xtype: 'displayfield',
+				fieldLabel: gettext('Color Overrides'),
+			    },
+			    {
+				name: 'colors',
+				xtype: 'pveTagColorGrid',
+				deleteEmpty: true,
+				height: 300,
+			    },
+			],
+		    },
+		],
+	    },
+	};
 
 	me.selModel = Ext.create('Ext.selection.RowModel', {});
 
@@ -347,6 +428,9 @@ Ext.define('PVE.dc.OptionView', {
 	    if (rec.data.value === '__default__') {
 		delete PVE.UIOptions.console;
 	    }
+
+	    let tagStyle = store.getById('tag-style')?.data?.value;
+	    PVE.Utils.updateTagSettings(tagStyle?.colors, tagStyle?.['tree-shape']);
 	});
 
 	me.on('activate', me.rstore.startUpdate);
-- 
2.30.2





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

* [pve-devel] [PATCH manager v8 08/12] ui: add form/Tag
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (14 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 07/12] ui: dc/OptionView: add editors for tag settings Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 09/12] ui: add form/TagEdit.js Dominik Csapak
                   ` (5 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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.

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

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 60ae421e..9d610f71 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..aa6ae867
--- /dev/null
+++ b/www/manager6/form/Tag.js
@@ -0,0 +1,233 @@
+Ext.define('Proxmox.Tag', {
+    extend: 'Ext.Component',
+    alias: 'widget.pmxTag',
+
+    mode: 'editable',
+
+    icons: {
+	editable: 'fa fa-minus-square',
+	normal: '',
+	inEdit: 'fa fa-check-square',
+    },
+
+    tag: '',
+    cls: 'pve-edit-tag',
+
+    tpl: [
+	'<i class="handle fa fa-bars"></i>',
+	'<span>{tag}</span>',
+	'<i class="action {iconCls}"></i>',
+    ],
+
+    // 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' || event.target.classList.contains('handle')) {
+	    return;
+	}
+	switch (me.mode) {
+	    case 'editable':
+		me.setVisible(false);
+		me.setTag('');
+		break;
+	    case 'inEdit':
+		me.setTag(me.tagEl().innerHTML);
+		me.setMode('editable');
+		break;
+	    default: break;
+	}
+    },
+
+    onClick: function(event) {
+	let me = this;
+	if (event.target.tagName !== 'SPAN' || me.mode !== 'editable') {
+	    return;
+	}
+	me.setMode('inEdit');
+
+	// select text in the element
+	let tagEl = me.tagEl();
+	tagEl.contentEditable = true;
+	let range = document.createRange();
+	range.selectNodeContents(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.setTag(rec.data.tag);
+			me.setMode('editable');
+			me.picker.hide();
+		    },
+		},
+	    });
+	}
+	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);
+    },
+
+    setMode: function(mode) {
+	let me = this;
+	if (me.icons[mode] === undefined) {
+	    throw "invalid mode";
+	}
+	let tagEl = me.tagEl();
+	if (tagEl) {
+	    tagEl.contentEditable = mode === 'inEdit';
+	}
+	me.removeCls(me.mode);
+	me.addCls(mode);
+	me.mode = mode;
+	me.updateData();
+    },
+
+    onKeyPress: function(event) {
+	let me = this;
+	let key = event.browserEvent.key;
+	switch (key) {
+	    case 'Enter':
+		if (me.tagEl().innerHTML !== '') {
+		    me.setTag(me.tagEl().innerHTML);
+		    me.setMode('editable');
+		    return;
+		}
+		break;
+	    case 'Escape':
+		me.cancelEdit();
+		return;
+	    case 'Backspace':
+	    case 'Delete':
+		return;
+	    default:
+		if (key.match(PVE.Utils.tagCharRegex)) {
+		    return;
+		}
+	}
+	event.browserEvent.preventDefault();
+	event.browserEvent.stopPropagation();
+    },
+
+    beforeInput: function(event) {
+	let me = this;
+	me.updateLayout();
+	let tag = event.event.data ?? event.event.dataTransfer?.getData('text/plain');
+	if (!tag) {
+	    return;
+	}
+	if (tag.match(PVE.Utils.tagCharRegex) === null) {
+	    event.event.preventDefault();
+	    event.event.stopPropagation();
+	}
+    },
+
+    onInput: function(event) {
+	let me = this;
+	me.picker.getStore().filter({
+	    property: 'tag',
+	    value: me.tagEl().innerHTML,
+	    anyMatch: true,
+	});
+    },
+
+    cancelEdit: function(list, event) {
+	let me = this;
+	if (me.mode === 'inEdit') {
+	    me.setTag(me.tag);
+	    me.setMode('editable');
+	}
+	me.picker?.hide();
+    },
+
+
+    setTag: function(tag) {
+	let me = this;
+	let oldtag = me.tag;
+	me.tag = tag;
+	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');
+	}
+	me.updateData();
+	if (oldtag !== tag) {
+	    me.fireEvent('change', me, tag, oldtag);
+	}
+    },
+
+    updateData: function() {
+	let me = this;
+	if (me.destroying || me.destroyed) {
+	    return;
+	}
+	me.update({
+	    tag: me.tag,
+	    iconCls: me.icons[me.mode],
+	});
+    },
+
+    tagEl: function() {
+	return this.el?.dom?.getElementsByTagName('span')?.[0];
+    },
+
+    listeners: {
+	mousedown: 'onMouseDown',
+	click: 'onClick',
+	focusleave: 'cancelEdit',
+	keydown: 'onKeyPress',
+	beforeInput: 'beforeInput',
+	input: 'onInput',
+	element: 'el',
+	scope: 'this',
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	me.setTag(me.tag);
+	me.setMode(me.mode ?? 'normal');
+	me.callParent();
+	me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => { me.setTag(me.tag); }); // refresh tag color
+    },
+
+    destroy: function() {
+	let me = this;
+	if (me.picker) {
+	    Ext.destroy(me.picker);
+	}
+	me.callParent();
+    },
+});
-- 
2.30.2





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

* [pve-devel] [PATCH manager v8 09/12] ui: add form/TagEdit.js
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (15 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 08/12] ui: add form/Tag Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
                   ` (4 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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

to add a new tag, we reuse the 'pmxTag' class, but overwrite some of
its behaviour so that it properly adds tags

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

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 9d610f71..eb4be4c5 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..1d832728
--- /dev/null
+++ b/www/manager6/form/TagEdit.js
@@ -0,0 +1,316 @@
+Ext.define('PVE.panel.TagEditContainer', {
+    extend: 'Ext.container.Container',
+    alias: 'widget.pveTagEditContainer',
+
+    layout: {
+	type: 'hbox',
+	align: 'stretch',
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	loadTags: function(tagstring = '', force = false) {
+	    let me = this;
+	    let view = me.getView();
+
+	    if (me.oldTags === tagstring && !force) {
+		return;
+	    }
+
+	    view.suspendLayout = true;
+	    me.forEachTag((tag) => {
+		view.remove(tag);
+	    });
+	    me.getViewModel().set('tagCount', 0);
+	    let newtags = tagstring.split(/[;, ]/).filter((t) => !!t) || [];
+	    newtags.forEach((tag) => {
+		me.addTag(tag);
+	    });
+	    view.suspendLayout = false;
+	    view.updateLayout();
+	    if (!force) {
+		me.oldTags = tagstring;
+	    }
+	},
+
+	onRender: function(v) {
+	    let me = this;
+	    let view = me.getView();
+	    view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), {
+		getDragData: function(e) {
+		    let source = e.getTarget('.handle');
+		    if (!source) {
+			return undefined;
+		    }
+		    let sourceId = source.parentNode.id;
+		    let cmp = Ext.getCmp(sourceId);
+		    let ddel = document.createElement('div');
+		    ddel.classList.add('proxmox-tags-full');
+		    ddel.innerHTML = Proxmox.Utils.getTagElement(cmp.tag, PVE.Utils.tagOverrides);
+		    let repairXY = Ext.fly(source).getXY();
+		    cmp.setDisabled(true);
+		    ddel.id = Ext.id();
+		    return {
+			ddel,
+			repairXY,
+			sourceId,
+		    };
+		},
+		onMouseUp: function(target, e, id) {
+		    let cmp = Ext.getCmp(this.dragData.sourceId);
+		    if (cmp && !cmp.isDestroyed) {
+			cmp.setDisabled(false);
+		    }
+		},
+		getRepairXY: function() {
+		    return this.dragData.repairXY;
+		},
+		beforeInvalidDrop: function(target, e, id) {
+		    let cmp = Ext.getCmp(this.dragData.sourceId);
+		    if (cmp && !cmp.isDestroyed) {
+			cmp.setDisabled(false);
+		    }
+		},
+	    });
+	    view.dropzone = Ext.create('Ext.dd.DropZone', v.getEl(), {
+		getTargetFromEvent: function(e) {
+		    return e.getTarget('.proxmox-tag-dark,.proxmox-tag-light');
+		},
+		getIndicator: function() {
+		    if (!view.indicator) {
+			view.indicator = Ext.create('Ext.Component', {
+			    floating: true,
+			    html: '<i class="fa fa-long-arrow-up"></i>',
+			    hidden: true,
+			    shadow: false,
+			});
+		    }
+		    return view.indicator;
+		},
+		onContainerOver: function() {
+		    this.getIndicator().setVisible(false);
+		},
+		notifyOut: function() {
+		    this.getIndicator().setVisible(false);
+		},
+		onNodeOver: function(target, dd, e, data) {
+		    let indicator = this.getIndicator();
+		    indicator.setVisible(true);
+		    indicator.alignTo(Ext.getCmp(target.id), 't50-bl', [-1, -2]);
+		    return this.dropAllowed;
+		},
+		onNodeDrop: function(target, dd, e, data) {
+		    this.getIndicator().setVisible(false);
+		    let sourceCmp = Ext.getCmp(data.sourceId);
+		    if (!sourceCmp) {
+			return;
+		    }
+		    sourceCmp.setDisabled(false);
+		    let targetCmp = Ext.getCmp(target.id);
+		    view.remove(sourceCmp, { destroy: false });
+		    view.insert(view.items.indexOf(targetCmp), sourceCmp);
+		},
+	    });
+	},
+
+	forEachTag: function(func) {
+	    let me = this;
+	    let view = me.getView();
+	    view.items.each((field) => {
+		if (field.reference === 'addTagBtn') {
+		    return false;
+		}
+		if (field.getXType() === 'pmxTag') {
+		    func(field);
+		}
+		return true;
+	    });
+	},
+
+	toggleEdit: function(cancel) {
+	    let me = this;
+	    let vm = me.getViewModel();
+	    let editMode = !vm.get('editMode');
+	    vm.set('editMode', editMode);
+
+	    me.forEachTag((tag) => {
+		tag.setMode(editMode ? 'editable' : 'normal');
+	    });
+
+	    if (!vm.get('editMode')) {
+		let tags = [];
+		if (cancel) {
+		    me.loadTags(me.oldTags, true);
+		} else {
+		    me.forEachTag((cmp) => {
+			if (cmp.isVisible() && cmp.tag) {
+			    tags.push(cmp.tag);
+			}
+		    });
+		    tags = tags.join(',');
+		    if (me.oldTags !== tags) {
+			me.oldTags = tags;
+			me.getView().fireEvent('change', tags);
+		    }
+		}
+	    }
+	    me.getView().updateLayout();
+	},
+
+	addTag: function(tag) {
+	    let me = this;
+	    let view = me.getView();
+	    let vm = me.getViewModel();
+	    let index = view.items.indexOf(me.lookup('addTagBtn'));
+	    view.insert(index, {
+		xtype: 'pmxTag',
+		tag,
+		mode: vm.get('editMode') ? 'editable' : 'normal',
+		listeners: {
+		    change: (field, newTag) => {
+			if (newTag === '') {
+			    view.remove(field);
+			    vm.set('tagCount', vm.get('tagCount') - 1);
+			}
+		    },
+		},
+	    });
+
+	    vm.set('tagCount', vm.get('tagCount') + 1);
+	},
+
+	addTagClick: function(event) {
+	    let me = this;
+	    if (event.target.tagName === 'SPAN') {
+		me.lookup('addTagBtn').tagEl().innerHTML = '';
+		me.lookup('addTagBtn').updateLayout();
+	    }
+	},
+
+	addTagMouseDown: function(event) {
+	    let me = this;
+	    if (event.target.tagName === 'I') {
+		let tag = me.lookup('addTagBtn').tagEl().innerHTML;
+		if (tag !== '') {
+		    me.addTag(tag, true);
+		}
+	    }
+	},
+
+	addTagChange: function(field, tag) {
+	    let me = this;
+	    if (tag !== '') {
+		me.addTag(tag, true);
+	    }
+	    field.tag = '';
+	},
+
+	cancelClick: function() {
+	    this.toggleEdit(true);
+	},
+
+	editClick: function() {
+	    this.toggleEdit(false);
+	},
+
+	init: function(view) {
+	    let me = this;
+	    if (view.tags) {
+		me.loadTags(view.tags);
+	    }
+	},
+    },
+
+    viewModel: {
+	data: {
+	    tagCount: 0,
+	    editMode: false,
+	},
+
+	formulas: {
+	    hideNoTags: function(get) {
+		return get('editMode') || get('tagCount') !== 0;
+	    },
+	    editBtnHtml: function(get) {
+		let cls = get('editMode') ? 'check' : 'pencil';
+		let qtip = get('editMode') ? gettext('Apply Changes') : gettext('Edit Tags');
+		return `<i data-qtip="${qtip}" class="fa fa-${cls}"></i>`;
+	    },
+	},
+    },
+
+    loadTags: function() {
+	return this.getController().loadTags(...arguments);
+    },
+
+    items: [
+	{
+	    xtype: 'box',
+	    bind: {
+		hidden: '{hideNoTags}',
+	    },
+	    html: gettext('No Tags'),
+	},
+	{
+	    xtype: 'pmxTag',
+	    reference: 'addTagBtn',
+	    cls: 'pve-add-tag',
+	    mode: 'editable',
+	    tag: '',
+	    tpl: `<span>${gettext('Add Tag')}</span><i class="action fa fa-plus-square"></i>`,
+	    bind: {
+		hidden: '{!editMode}',
+	    },
+	    hidden: true,
+	    onMouseDown: Ext.emptyFn, // prevent default behaviour
+	    listeners: {
+		click: {
+		    element: 'el',
+		    fn: 'addTagClick',
+		},
+		mousedown: {
+		    element: 'el',
+		    fn: 'addTagMouseDown',
+		},
+		change: 'addTagChange',
+	    },
+	},
+	{
+	    xtype: 'box',
+	    html: `<i data-qtip="${gettext('Cancel')}" class="fa fa-times"></i>`,
+	    cls: 'pve-tag-inline-button',
+	    hidden: true,
+	    bind: {
+		hidden: '{!editMode}',
+	    },
+	    listeners: {
+		click: 'cancelClick',
+		element: 'el',
+	    },
+	},
+	{
+	    xtype: 'box',
+	    cls: 'pve-tag-inline-button',
+	    bind: {
+		html: '{editBtnHtml}',
+	    },
+	    listeners: {
+		click: 'editClick',
+		element: 'el',
+	    },
+	},
+    ],
+
+    listeners: {
+	render: 'onRender',
+    },
+
+    destroy: function() {
+	let me = this;
+	Ext.destroy(me.dragzone);
+	Ext.destroy(me.dropzone);
+	Ext.destroy(me.indicator);
+	me.callParent();
+    },
+});
-- 
2.30.2





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

* [pve-devel] [PATCH manager v8 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (16 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 09/12] ui: add form/TagEdit.js Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 11/12] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
                   ` (3 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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 93f385db..9b3017ad 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] 26+ messages in thread

* [pve-devel] [PATCH manager v8 11/12] ui: tree/ResourceTree: show Tags in tree
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (17 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 12/12] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
                   ` (2 subsequent siblings)
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 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 9b3017ad..f3339051 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] 26+ messages in thread

* [pve-devel] [PATCH manager v8 12/12] ui: add tags to ResourceGrid and GlobalSearchField
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (18 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 11/12] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
@ 2022-10-18 14:02 ` Dominik Csapak
  2022-11-07 14:56 ` [pve-devel] [PATCH manager] ui: add missing tag classes Dominik Csapak
  2022-11-09 13:11 ` [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Aaron Lauterer
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-10-18 14:02 UTC (permalink / raw)
  To: pve-devel

also allows to search for tags in the GlobalSearchField where 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>

ui: ResourceGrid: render tags

with the 'full' styling

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/data/ResourceStore.js     |  1 +
 www/manager6/form/GlobalSearchField.js | 20 +++++++++++++++-----
 www/manager6/grid/ResourceGrid.js      |  1 +
 3 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/www/manager6/data/ResourceStore.js b/www/manager6/data/ResourceStore.js
index b18f7dd8..ed1f4699 100644
--- a/www/manager6/data/ResourceStore.js
+++ b/www/manager6/data/ResourceStore.js
@@ -295,6 +295,7 @@ Ext.define('PVE.data.ResourceStore', {
 	    },
 	    tags: {
 		header: gettext('Tags'),
+		renderer: (value) => PVE.Utils.renderTags(value, PVE.Utils.tagOverrides),
 		type: 'string',
 		hidden: true,
 		sortable: true,
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) {
diff --git a/www/manager6/grid/ResourceGrid.js b/www/manager6/grid/ResourceGrid.js
index 29906a37..9376bcc2 100644
--- a/www/manager6/grid/ResourceGrid.js
+++ b/www/manager6/grid/ResourceGrid.js
@@ -7,6 +7,7 @@ Ext.define('PVE.grid.ResourceGrid', {
 	property: 'type',
 	direction: 'ASC',
     },
+    userCls: 'proxmox-tags-full',
     initComponent: function() {
 	let me = this;
 
-- 
2.30.2





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

* [pve-devel] [PATCH manager] ui: add missing tag classes
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (19 preceding siblings ...)
  2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 12/12] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
@ 2022-11-07 14:56 ` Dominik Csapak
  2022-11-09 13:11 ` [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Aaron Lauterer
  21 siblings, 0 replies; 26+ messages in thread
From: Dominik Csapak @ 2022-11-07 14:56 UTC (permalink / raw)
  To: pve-devel

for the tags ui to properly work, these css classes are necessary

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
this patch was missing in my last tags gui series, somehow i messed up
during rebase and these lines got lost

if i need to resend the series, i fixup the correct patches with the
css styles

thanks @aaron for noticing something was off :)

 www/css/ext6-pve.css | 34 ++++++++++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index f7d0c420..7b91871d 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -656,3 +656,37 @@ table.osds td:first-of-type {
     padding-top: 0px;
     padding-bottom: 0px;
 }
+
+.pve-edit-tag > i,
+.pve-add-tag > i {
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.pve-edit-tag > i.handle {
+    padding-right: 5px;
+}
+
+.pve-edit-tag > i.action,
+.pve-add-tag > i.action {
+    padding-left: 5px;
+}
+
+.pve-edit-tag > i.handle {
+    cursor: grab;
+}
+
+.pve-edit-tag.normal > i,
+.pve-edit-tag.inEdit > i.handle {
+    display: none;
+}
+
+.pve-add-tag {
+    background-color: #f5f5f5 ! important;
+    color: #000000 ! important;
+}
+
+.pve-tag-inline-button {
+    cursor: pointer;
+    padding-left: 2px;
+}
-- 
2.30.2





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

* Re: [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui
  2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
                   ` (20 preceding siblings ...)
  2022-11-07 14:56 ` [pve-devel] [PATCH manager] ui: add missing tag classes Dominik Csapak
@ 2022-11-09 13:11 ` Aaron Lauterer
  21 siblings, 0 replies; 26+ messages in thread
From: Aaron Lauterer @ 2022-11-09 13:11 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

I did test it for a bit, and it seems to work a lot better than the last 
iteration :)

 From a high level user perspective, I would like to see the following:

- a list of configured tags that I am allowed to set in the drop down (the 
user-tag-privileges list)

- better error messages that tell me which tag caused which problem. For example 
something like: "tag foo is not in list of allowed tags" or "tag bar is reserved 
for admins"

- as user, no GUI action buttons on admin tags


Another thing that I noticed, which is not exclusive to this patch series, is 
that a user without permissions can still open the options in the 
cluster->options panel and will only get an error when trying to save the 
changes. Not allowing to open the edit windows could probably be a good idea.


Will go through the code and add some more smaller details that I found in the 
respective patches




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

* Re: [pve-devel] [PATCH cluster v8 4/4] DataCenterConfig: add tag rights control to the datacenter config
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 4/4] DataCenterConfig: add tag rights control to the datacenter config Dominik Csapak
@ 2022-11-09 14:42   ` Aaron Lauterer
  2022-11-10 10:09   ` Thomas Lamprecht
  1 sibling, 0 replies; 26+ messages in thread
From: Aaron Lauterer @ 2022-11-09 14:42 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak



On 10/18/22 16:02, Dominik Csapak wrote:
> by adding a 'user-tag-privileges' and 'admin-tags' option.
> The first sets the policy by which "normal" users (with
> 'VM.Config.Options' on the respective guest) can create/delete tags
> and the second is a list of tags only settable by 'admins'
> ('Sys.Modify' on '/')
> 
> also add a helper 'get_user_admin_tags' that returns two hashmaps that
> determines the allowed user tags and admin tags that require elevated
> permissions
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
>   data/PVE/DataCenterConfig.pm | 93 ++++++++++++++++++++++++++++++++++++
>   1 file changed, 93 insertions(+)
> 
> diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
> index bb29d26..e2140ff 100644
> --- a/data/PVE/DataCenterConfig.pm
> +++ b/data/PVE/DataCenterConfig.pm
> @@ -154,6 +154,26 @@ my $tag_style_format = {
>       },
>   };
>   
> +my $user_tag_privs_format = {
> +    'usable' => {
> +	optional => 1,
> +	type => 'string',
> +	enum => ['none', 'list', 'existing', 'free'],
> +	default => 'free',
> +	dscription => "Determines which tags a user without Sys.Modify on '/' can set and delete. ".

s/dscription/description/

> +	    "'none' means no tags are settable.'list' allows tags from the given list. ".
> +	    "'existing' means only already existing tags or from the given list. ".
> +	    "And 'free' means users can assign any tags."
> +    },
> +    'list' => {
> +	optional => 1,
> +	type => 'string',
> +	pattern => "${PVE::JSONSchema::PVE_TAG_RE}(?:\;${PVE::JSONSchema::PVE_TAG_RE})*",
> +	typetext => "<tag>[;<tag>=...]",
> +	description => "List of tags users are allowd to set and delete (semicolon separated).",
> +    },
> +};
> +
>   my $datacenter_schema = {
>       type => "object",
>       additionalProperties => 0,
> @@ -285,12 +305,60 @@ my $datacenter_schema = {
>   	    description => "Tag style options.",
>   	    format => $tag_style_format,
>   	},
> +	'user-tag-privileges' => {
> +	    optional => 1,
> +	    type => 'string',
> +	    description => "Privilege options for user settable tags",
> +	    format => $user_tag_privs_format,
> +	},
> +	'admin-tags' => {
> +	    optional => 1,
> +	    type => 'string',
> +	    description => "A list of tags only admins (Sys.Modify on '/') are allowed to set/delete",
> +	    pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
> +	},
>       },
>   };
>   

Is it possible to add a "typetext" for admin-tags as well? The `pvesh usage 
--verbose` output for the parameter looks rather confusing.

>   # make schema accessible from outside (for documentation)
>   sub get_datacenter_schema { return $datacenter_schema };
>   
> +# returns two hashmaps of tags, the first is the list of tags that can
> +# be used by users with 'VM.Config.Options', and the second is a list
> +# that needs 'Sys.Modify' on '/'
> +#
> +# If the first map is 'undef', it means there is generally no restriction
> +# besides the tags defined in the second map.
> +#
> +# CAUTION: this function may include tags from *all* guest configs,
> +# regardless of the current authuser
> +sub get_user_admin_tags {
> +    my $user_tags = {};
> +    my $admin_tags = {};
> +
> +    my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
> +    if (my $user_tag_privs = $dc->{'user-tag-privileges'}) {
> +	my $usable = $user_tag_privs->{usable} // 'free';
> +	if ($usable eq 'free') {
> +	    $user_tags = undef;
> +	} elsif ($usable eq 'existing') {
> +	    map { $user_tags->{$_} = 1 } ($user_tag_privs->{list} // [])->@*;
> +	    my $props = PVE::Cluster::get_guest_config_properties(['tags']);
> +	    for my $vmid (keys $props->%*) {
> +		map { $user_tags->{$_} = 1 } PVE::Tools::split_list($props->{$vmid}->{tags});

Am I right that a permission check to only add the tags from guests for which 
the user has the necessary permissions is computationally quite expensive?

I see two potential use cases here. One, a large organization that splits up 
access for mgmt but would like to use the same tags throughout for consistency. 
So getting all tags is fine.
The other would be one cluster with direct access for mgmt by different 
customers. Seeing all the tags configured in the cluster could leak some private 
information, depending on what tags have been assigned.




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

* Re: [pve-devel] [PATCH cluster v8 3/4] datacenter.cfg: add option for tag-style
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 3/4] datacenter.cfg: add option for tag-style Dominik Csapak
@ 2022-11-10  9:54   ` Thomas Lamprecht
  0 siblings, 0 replies; 26+ messages in thread
From: Thomas Lamprecht @ 2022-11-10  9:54 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

w.r.t. to this one and the next patch:

I'd slightly prefer either having just a single property for all, or split it into two:
style and rest. The latter may have a small benefit w.r.t. to stability - if a parser/edit
bug sneaks in that breaks styling the access stuff might be still working, but depends
really on what scenario one imagines.

so either:

tags => {
   ...
    format => {
        'color-map' => {},
        'shape' => {},
        'access' => {},
        'access-list' => {}
        'registered' => {} # admin tags 
    },
}

(I also altered the property names a bit to drop the slightly redundant "tag" from the
inner ones and use registered for admin tags (no hard feelings on the latter one))

or

'tag-style' => {
   ...
    format => {
        'color-map' => {},
        'shape' => {},
    },
},
'tag-access' => {
   ...
    format => {
        'user-allow' => {},
        'user-allow-list' => {}
        'registered' => {} # admin tags, if not separate.
    },
},

but I don't think the tree properties are bad at all, just seems a bit bloated, probably
just because we rarely expand datacenter.cfg. So, I won't block this if you want to stick
to your originally formats.




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

* Re: [pve-devel] [PATCH cluster v8 4/4] DataCenterConfig: add tag rights control to the datacenter config
  2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 4/4] DataCenterConfig: add tag rights control to the datacenter config Dominik Csapak
  2022-11-09 14:42   ` Aaron Lauterer
@ 2022-11-10 10:09   ` Thomas Lamprecht
  1 sibling, 0 replies; 26+ messages in thread
From: Thomas Lamprecht @ 2022-11-10 10:09 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

Am 18/10/2022 um 16:02 schrieb Dominik Csapak:
> by adding a 'user-tag-privileges' and 'admin-tags' option.
> The first sets the policy by which "normal" users (with
> 'VM.Config.Options' on the respective guest) can create/delete tags
> and the second is a list of tags only settable by 'admins'
> ('Sys.Modify' on '/')
> 
> also add a helper 'get_user_admin_tags' that returns two hashmaps that
> determines the allowed user tags and admin tags that require elevated
> permissions
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
>  data/PVE/DataCenterConfig.pm | 93 ++++++++++++++++++++++++++++++++++++
>  1 file changed, 93 insertions(+)
> 
> diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
> index bb29d26..e2140ff 100644
> --- a/data/PVE/DataCenterConfig.pm
> +++ b/data/PVE/DataCenterConfig.pm
> @@ -154,6 +154,26 @@ my $tag_style_format = {
>      },
>  };
>  
> +my $user_tag_privs_format = {
> +    'usable' => {
> +	optional => 1,
> +	type => 'string',
> +	enum => ['none', 'list', 'existing', 'free'],
> +	default => 'free',
> +	dscription => "Determines which tags a user without Sys.Modify on '/' can set and delete. ".
> +	    "'none' means no tags are settable.'list' allows tags from the given list. ".
> +	    "'existing' means only already existing tags or from the given list. ".
> +	    "And 'free' means users can assign any tags."

could be split into a "description" (for CLI usage) and a "verbose_description" (for man page/docs),
something like:

description => "Controls tag usage for users without `Sys.Modify` on `/` by either"
    ." allowing `none`, a `list`, already `existing` (used) or anything (`free`).",
verbose_description => "Controls which tags can be set or deleted on resources an user
    ." controls (such as guests). Users with the `Sys.Modify` privilege on `/` are always unrestricted."
    ."* `none`: ..."
    ."* `list`: ..."
    ."* `existing`: ..."
    ."* `free`: ...",

> +    },
> +    'list' => {
> +	optional => 1,
> +	type => 'string',
> +	pattern => "${PVE::JSONSchema::PVE_TAG_RE}(?:\;${PVE::JSONSchema::PVE_TAG_RE})*",
> +	typetext => "<tag>[;<tag>=...]",
> +	description => "List of tags users are allowd to set and delete (semicolon separated).",
> +    },
> +};
> +
>  my $datacenter_schema = {
>      type => "object",
>      additionalProperties => 0,
> @@ -285,12 +305,60 @@ my $datacenter_schema = {
>  	    description => "Tag style options.",
>  	    format => $tag_style_format,
>  	},
> +	'user-tag-privileges' => {
> +	    optional => 1,
> +	    type => 'string',
> +	    description => "Privilege options for user settable tags",
> +	    format => $user_tag_privs_format,
> +	},
> +	'admin-tags' => {
> +	    optional => 1,
> +	    type => 'string',
> +	    description => "A list of tags only admins (Sys.Modify on '/') are allowed to set/delete",
> +	    pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
> +	},
>      },
>  };
>  
>  # make schema accessible from outside (for documentation)
>  sub get_datacenter_schema { return $datacenter_schema };
>  
> +# returns two hashmaps of tags, the first is the list of tags that can

returns a tuple of two hash maps with tags as keys, ...

> +# be used by users with 'VM.Config.Options', and the second is a list

be used with just 'VM.Config.Options' on '/vms/{vmid}'

> +# that needs 'Sys.Modify' on '/'
> +#
> +# If the first map is 'undef', it means there is generally no restriction
> +# besides the tags defined in the second map.
> +#
> +# CAUTION: this function may include tags from *all* guest configs,
> +# regardless of the current authuser
> +sub get_user_admin_tags {

hmm, sounds a bit confusing, mabye one of:

* add and: get_user_and_admin_tags

* get_unrestricted_and_registered_tags

* or just get_allowed_tags (with the comment highlighting that it returns two, the
  allowed for all, and the one for "admins" it would be quite clear and also short)





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

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

Thread overview: 26+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-10-18 14:02 [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 1/4] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 2/4] Cluster: add get_guest_config_properties Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 3/4] datacenter.cfg: add option for tag-style Dominik Csapak
2022-11-10  9:54   ` Thomas Lamprecht
2022-10-18 14:02 ` [pve-devel] [PATCH cluster v8 4/4] DataCenterConfig: add tag rights control to the datacenter config Dominik Csapak
2022-11-09 14:42   ` Aaron Lauterer
2022-11-10 10:09   ` Thomas Lamprecht
2022-10-18 14:02 ` [pve-devel] [PATCH qemu-server v8] api: update: improve tag privilege check Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH container v8] check_ct_modify_config_perm: " Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH widget-toolkit v8 1/2] add tag related helpers Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH widget-toolkit v8 2/2] Toolkit: add override for Ext.dd.DragDropManager Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 01/12] api: /cluster/resources: add tags to returned properties Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 02/12] api: add /ui-options api call Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 03/12] ui: call '/ui-options' and save the result in PVE.UIOptions Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 04/12] ui: parse and save tag color overrides from /ui-options Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 05/12] ui: tree/ResourceTree: collect tags on update Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 06/12] ui: add form/TagColorGrid Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 07/12] ui: dc/OptionView: add editors for tag settings Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 08/12] ui: add form/Tag Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 09/12] ui: add form/TagEdit.js Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 10/12] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 11/12] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
2022-10-18 14:02 ` [pve-devel] [PATCH manager v8 12/12] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
2022-11-07 14:56 ` [pve-devel] [PATCH manager] ui: add missing tag classes Dominik Csapak
2022-11-09 13:11 ` [pve-devel] [PATCH cluster/qemu-server/container/wt/manager v8] add tags to ui Aaron Lauterer

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