public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui
@ 2022-11-16 15:47 Dominik Csapak
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
                   ` (22 more replies)
  0 siblings, 23 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:47 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 tags will be shown
* by default, the color is (consistently) autogenerated based on the
  text
* that color can be overridden 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-access' and 'registered-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 v10:
* adapted c code to wolfgangs requests
  - make the config \n terminated if it's not already
  - comment that lines must be null terminated
  - better types
* renamed 'privileged-tags' to 'registered-tags' (also in the ui)
* moved 'get_allowed_tags' into pve-guest-common
* internal improvements in assert_tag_permissions
* made alphabetical ordering the default (@thomas request)
* removed /ui-options, reused /cluster/options for that
* show warning in gui when tag is in allow list and registered
* reworded the descriptions in DataCenterConfig
* some minor gui changes
  - gettext fixes
  - renaming pmxTag to pveTag
  - refactored the dc edit windows out into their own files
* omitted already applied widget-toolkit patches

changes from v9:
* implemented changes required by wolfgang
  - fixed some c code behaviour
  - refactored the permission check into guest-common
* changed a bit how the permission check worked:
  - get_allowed_tags now takes a bool (privileged_user) and a closure
    for checking the vm access. with this we can avoid a
    cluster <-> access-control cyclic dependency
  - refactored some of the internal code of the permission check
    (see patch for details)
  - the forbidden tags are now in quotes in the error
* added an optional 'ordering' option into the 'tag-style' property
  with this the tags will be sorted in the gui (ui only)
  i left the patches separately

(omitted older changelog)

pve-cluster:

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

 data/PVE/Cluster.pm          |  27 +++++
 data/PVE/DataCenterConfig.pm | 109 ++++++++++++++++++
 data/src/cfs-ipc-ops.h       |   2 +
 data/src/server.c            |  63 +++++++++++
 data/src/status.c            | 207 +++++++++++++++++++++++++----------
 data/src/status.h            |   3 +
 6 files changed, 353 insertions(+), 58 deletions(-)

pve-guest-common:

Dominik Csapak (1):
  GuestHelpers: add tag related helpers

 debian/control          |  3 +-
 src/PVE/GuestHelpers.pm | 99 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 100 insertions(+), 2 deletions(-)

qemu-server:

Dominik Csapak (1):
  api: update: check for tags permissions with 'assert_tag_permissions'

 PVE/API2/Qemu.pm | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

pve-container:

Dominik Csapak (1):
  check_ct_modify_config_perm: check for tags permissions with
    'assert_tag_permissions'

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

pve-manager:

Dominik Csapak (13):
  api: /cluster/resources: add tags to returned properties
  api: allow all users to (partially) read datacenter.cfg
  ui: save ui options from /cluster/options instead of version
  ui: parse and save tag infos from /cluster/options
  ui: add form/TagColorGrid
  ui: add PVE.form.ListField
  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
  ui: implement tag ordering from datacenter.cfg

 PVE/API2.pm                            |   3 +-
 PVE/API2/Cluster.pm                    |  33 ++-
 www/css/ext6-pve.css                   |  57 ++++
 www/manager6/Makefile                  |   6 +
 www/manager6/Utils.js                  | 104 ++++++-
 www/manager6/Workspace.js              |   2 +
 www/manager6/data/ResourceStore.js     |   7 +
 www/manager6/dc/OptionView.js          | 139 +++++++++-
 www/manager6/dc/RegisteredTagsEdit.js  |  70 +++++
 www/manager6/dc/UserTagAccessEdit.js   |  90 +++++++
 www/manager6/form/GlobalSearchField.js |  20 +-
 www/manager6/form/ListField.js         | 165 ++++++++++++
 www/manager6/form/Tag.js               | 232 ++++++++++++++++
 www/manager6/form/TagColorGrid.js      | 357 +++++++++++++++++++++++++
 www/manager6/form/TagEdit.js           | 336 +++++++++++++++++++++++
 www/manager6/grid/ResourceGrid.js      |   1 +
 www/manager6/lxc/Config.js             |  36 ++-
 www/manager6/qemu/Config.js            |  35 ++-
 www/manager6/tree/ResourceTree.js      |  10 +-
 19 files changed, 1682 insertions(+), 21 deletions(-)
 create mode 100644 www/manager6/dc/RegisteredTagsEdit.js
 create mode 100644 www/manager6/dc/UserTagAccessEdit.js
 create mode 100644 www/manager6/form/ListField.js
 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] 25+ messages in thread

* [pve-devel] [PATCH cluster v11 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
@ 2022-11-16 15:47 ` Dominik Csapak
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 2/5] Cluster: add get_guest_config_properties Dominik Csapak
                   ` (21 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:47 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      |  63 +++++++++++++
 data/src/status.c      | 207 +++++++++++++++++++++++++++++------------
 data/src/status.h      |   3 +
 4 files changed, 217 insertions(+), 58 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..80f838b 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,62 @@ 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 || current[proplen] != '\0') {
+				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;
+			    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 validity 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..2a54eaf 100644
--- a/data/src/status.c
+++ b/data/src/status.c
@@ -804,25 +804,63 @@ 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
+// note: line[line_end] needs to be guaranteed a null byte
+char *
+_get_property_value_from_line(char *line, size_t line_len, const char *prop, size_t 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];
+		char *v_end = &line[line_len - 1];
+
+		// drop initial value whitespaces here already
+		while (v_start < v_end && *v_start && isspace(*v_start)) v_start++;
+
+		if (!*v_start) return NULL;
+
+		while (v_end > v_start && isspace(*v_end)) v_end--;
+		if (v_end < &line[line_len - 1]) {
+		    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
+//
+// min..max is the char range of the first character of the given props,
+// so that we can return early when checking the line
+// note: conf must end with a newline
+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;
+	uint8_t count = 0;
+
+	if (conf_size == 0) {
+	    return;
+	}
 
 	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 +868,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;
-
-		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;
+		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;
+
+		size_t line_len = next_newline - line;
+		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 +912,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 +1001,15 @@ 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, '}');
 
+		// conf needs to be newline terminated
+		if (((char *)tmp)[size - 1] != '\n') {
+			gpointer new = realloc(tmp, size + 1);
+			if (new == NULL) goto err;
+			tmp = new;
+			((char *)tmp)[size++] = '\n';
+		}
+		_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 +1025,24 @@ 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;
-
-			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;
+			if (tmp == NULL) continue;
+
+			// conf needs to be newline terminated
+			if (((char *)tmp)[size - 1] != '\n') {
+			    gpointer new = realloc(tmp, size + 1);
+			    if (new == NULL) continue;
+			    tmp = new;
+			    ((char *)tmp)[size++] = '\n';
+			}
 
-			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 +1058,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] 25+ messages in thread

* [pve-devel] [PATCH cluster v11 2/5] Cluster: add get_guest_config_properties
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-11-16 15:47 ` Dominik Csapak
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 3/5] datacenter.cfg: add option for tag-style Dominik Csapak
                   ` (20 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:47 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] 25+ messages in thread

* [pve-devel] [PATCH cluster v11 3/5] datacenter.cfg: add option for tag-style
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 2/5] Cluster: add get_guest_config_properties Dominik Csapak
@ 2022-11-16 15:47 ` Dominik Csapak
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 4/5] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
                   ` (19 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:47 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..532e5e5 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 = {
+    'shape' => {
+	optional => 1,
+	type => 'string',
+	enum => ['full', 'circle', 'dense', 'none'],
+	default => 'circle',
+	description => "Tag shape for the web ui 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.",
+    },
+    'color-map' => {
+	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] 25+ messages in thread

* [pve-devel] [PATCH cluster v11 4/5] datacenter.cfg: add tag rights control to the datacenter config
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (2 preceding siblings ...)
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 3/5] datacenter.cfg: add option for tag-style Dominik Csapak
@ 2022-11-16 15:47 ` Dominik Csapak
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 5/5] datacenter.cfg: add 'ordering' to 'tag-style' config Dominik Csapak
                   ` (18 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:47 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_allowed_tags' that returns the allowed (existing)
tags, the privileged tags, and if a user can enter 'freeform' tags.

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

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 532e5e5..0836891 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -154,6 +154,32 @@ my $tag_style_format = {
     },
 };
 
+my $user_tag_privs_format = {
+    'user-allow' => {
+	optional => 1,
+	type => 'string',
+	enum => ['none', 'list', 'existing', 'free'],
+	default => 'free',
+	description => "Controls tag usage for users without `Sys.Modify` on `/` by either "
+	    ."allowing `none`, a `list`, already `existing` or anything (`free`).",
+	verbose_description => "Controls which tags can be set or deleted on resources a user "
+	    ."controls (such as guests). Users with the `Sys.Modify` privilege on `/` are always "
+	    ." unrestricted. "
+	    ."'none' no tags are usable. "
+	    ."'list' tasg from 'user-allow'list' are usable. "
+	    ."'existing' like list, but already existing tags of resources are also usable."
+	    ."'free' no tag restrictions."
+    },
+    'user-allow-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 allowed to set and delete (semicolon separated) "
+	    ."for 'user-allow' values 'list' and 'existing'.",
+    },
+};
+
 my $datacenter_schema = {
     type => "object",
     additionalProperties => 0,
@@ -285,6 +311,20 @@ my $datacenter_schema = {
 	    description => "Tag style options.",
 	    format => $tag_style_format,
 	},
+	'user-tag-access' => {
+	    optional => 1,
+	    type => 'string',
+	    description => "Privilege options for user-settable tags",
+	    format => $user_tag_privs_format,
+	},
+	'registered-tags' => {
+	    optional => 1,
+	    type => 'string',
+	    description => "A list of tags that require a `Sys.Modify` on '/' to set and delete. "
+		."Tags set here that are also in 'user-tag-access' also require `Sys.Modify`.",
+	    pattern => "(?:${PVE::JSONSchema::PVE_TAG_RE};)*${PVE::JSONSchema::PVE_TAG_RE}",
+	    typetext => "<tag>[;<tag>...]",
+	},
     },
 };
 
@@ -333,6 +373,19 @@ sub parse_datacenter_config {
 	$res->{'tag-style'} = parse_property_string($tag_style_format, $tag_style);
     }
 
+    if (my $user_tag_privs = $res->{'user-tag-access'}) {
+	$res->{'user-tag-access'} =
+	    parse_property_string($user_tag_privs_format, $user_tag_privs);
+
+	if (my $user_tags = $res->{'user-tag-access'}->{'user-allow-list'}) {
+	    $res->{'user-tag-access'}->{'user-allow-list'} = [split(';', $user_tags)];
+	}
+    }
+
+    if (my $admin_tags = $res->{'registered-tags'}) {
+	$res->{'registered-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 +449,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-access'})) {
+	if (my $user_tags = $user_tag_privs->{'user-allow-list'}) {
+	    $user_tag_privs->{'user-allow-list'} = join(';', sort $user_tags->@*);
+	}
+	$cfg->{'user-tag-access'} =
+	    PVE::JSONSchema::print_property_string($user_tag_privs, $user_tag_privs_format);
+    }
+
+    if (ref(my $admin_tags = $cfg->{'registered-tags'})) {
+	$cfg->{'registered-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] 25+ messages in thread

* [pve-devel] [PATCH cluster v11 5/5] datacenter.cfg: add 'ordering' to 'tag-style' config
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (3 preceding siblings ...)
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 4/5] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
@ 2022-11-16 15:47 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH guest-common v11 1/1] GuestHelpers: add tag related helpers Dominik Csapak
                   ` (17 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:47 UTC (permalink / raw)
  To: pve-devel

such that the admin can decide if the tags should be sorted in the gui

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

diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 0836891..f72974e 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -152,6 +152,13 @@ my $tag_style_format = {
 	typetext => '<tag>:<hex-color>[:<hex-color-for-text>][;<tag>=...]',
 	description => "Manual color mapping for tags (semicolon separated).",
     },
+    ordering => {
+	optional => 1,
+	type => 'string',
+	enum => ['config', 'alphabetical'],
+	default => 'alphabetical',
+	description => 'Controls the sorting of the tags in the web ui.',
+    }
 };
 
 my $user_tag_privs_format = {
-- 
2.30.2





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

* [pve-devel] [PATCH guest-common v11 1/1] GuestHelpers: add tag related helpers
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (4 preceding siblings ...)
  2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 5/5] datacenter.cfg: add 'ordering' to 'tag-style' config Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-17 12:12   ` [pve-devel] applied: " Thomas Lamprecht
  2022-11-16 15:48 ` [pve-devel] [PATCH qemu-server v11 1/1] api: update: check for tags permissions with 'assert_tag_permissions' Dominik Csapak
                   ` (16 subsequent siblings)
  22 siblings, 1 reply; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

'get_allowed_tags':
returns the allowed tags for the given user

'assert_tag_permissions'
helper to check permissions for tag setting/updating/deleting
for both container and qemu-server

gets the list of allowed tags from the DataCenterConfig and the current
user permissions, and checks for each tag that is added/removed if
the user has permissions to modify it

'normal' tags require 'VM.Config.Options' on '/vms/<vmid>', but not
allowed tags (either limited with 'user-tag-access' or
'privileged-tags' in the datacenter.cfg) requrie 'Sys.Modify' on '/'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 debian/control          |  3 +-
 src/PVE/GuestHelpers.pm | 99 ++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 100 insertions(+), 2 deletions(-)

diff --git a/debian/control b/debian/control
index 83bade3..ffff22b 100644
--- a/debian/control
+++ b/debian/control
@@ -12,7 +12,8 @@ Homepage: https://www.proxmox.com
 
 Package: libpve-guest-common-perl
 Architecture: all
-Depends: libpve-cluster-perl,
+Depends: libpve-access-control,
+         libpve-cluster-perl,
          libpve-common-perl (>= 7.2-6),
          libpve-storage-perl (>= 7.0-14),
          pve-cluster,
diff --git a/src/PVE/GuestHelpers.pm b/src/PVE/GuestHelpers.pm
index 0fe3fd6..a99a0ad 100644
--- a/src/PVE/GuestHelpers.pm
+++ b/src/PVE/GuestHelpers.pm
@@ -3,6 +3,7 @@ package PVE::GuestHelpers;
 use strict;
 use warnings;
 
+use PVE::Exception qw(raise_perm_exc);
 use PVE::Tools;
 use PVE::Storage;
 
@@ -11,7 +12,14 @@ use Scalar::Util qw(weaken);
 
 use base qw(Exporter);
 
-our @EXPORT_OK = qw(safe_string_ne safe_boolean_ne safe_num_ne typesafe_ne);
+our @EXPORT_OK = qw(
+assert_tag_permissions
+get_allowed_tags
+safe_boolean_ne
+safe_num_ne
+safe_string_ne
+typesafe_ne
+);
 
 # We use a separate lock to block migration while a replication job
 # is running.
@@ -246,4 +254,93 @@ sub config_with_pending_array {
     return $res;
 }
 
+# returns the allowed tags for the given user
+# in scalar context, returns the list of allowed tags that exist
+# in list context, returns a tuple of allowed tags, privileged tags, and if freeform is enabled
+#
+# first parameter is a bool if the user is 'privileged' (normally Sys.Modify on /)
+# second parameter is a closure which takes the vmid. should check if the user can see the vm tags
+sub get_allowed_tags {
+    my ($rpcenv, $user, $privileged_user) = @_;
+
+    $privileged_user //= ($rpcenv->check($user, '/', ['Sys.Modify'], 1) // 0);
+
+    my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
+
+    my $allowed_tags = {};
+    my $privileged_tags = {};
+    if (my $tags = $dc->{'registered-tags'}) {
+	$privileged_tags->{$_} = 1 for $tags->@*;
+    }
+    my $user_tag_privs = $dc->{'user-tag-access'} // {};
+    my $user_allow = $user_tag_privs->{'user-allow'} // 'free';
+    my $freeform = $user_allow eq 'free';
+
+    if ($user_allow ne 'none' || $privileged_user) {
+	$allowed_tags->{$_} = 1 for ($user_tag_privs->{'user-allow-list'} // [])->@*;
+    }
+
+    if ($user_allow eq 'free' || $user_allow eq 'existing' || $privileged_user) {
+	my $props = PVE::Cluster::get_guest_config_properties(['tags']);
+	for my $vmid (keys $props->%*) {
+	    next if !$privileged_user && !$rpcenv->check_vm_perm($user, $vmid, undef, ['VM.Audit'], 0, 1);
+	    $allowed_tags->{$_} = 1 for PVE::Tools::split_list($props->{$vmid}->{tags});
+	}
+    }
+
+    if ($privileged_user) {
+	$allowed_tags->{$_} = 1 for keys $privileged_tags->%*;
+    } else {
+	delete $allowed_tags->{$_} for keys $privileged_tags->%*;
+    }
+
+    return wantarray ? ($allowed_tags, $privileged_tags, $freeform) : $allowed_tags;
+}
+
+# checks the permissions for setting/updating/removing tags for guests
+# tagopt_old and tagopt_new expect the tags as they are in the config
+#
+# either returns gracefully or raises a permission exception
+sub assert_tag_permissions {
+    my ($vmid, $tagopt_old, $tagopt_new, $rpcenv, $authuser) = @_;
+
+    my $allowed_tags;
+    my $privileged_tags;
+    my $freeform;
+    my $privileged_user = $rpcenv->check($authuser, '/', ['Sys.Modify'], 1) // 0;
+
+    $rpcenv->check_vm_perm($authuser, $vmid, undef, ['VM.Config.Options']);
+
+    my $check_single_tag = sub {
+	my ($tag) = @_;
+	return if $privileged_user;
+
+	if (!defined($allowed_tags) && !defined($privileged_tags) && !defined($freeform)) {
+	    ($allowed_tags, $privileged_tags, $freeform) = get_allowed_tags(
+		$rpcenv,
+		$authuser,
+		$privileged_user,
+	    );
+	}
+
+	if ((!$allowed_tags->{$tag} && !$freeform) || $privileged_tags->{$tag}) {
+	    raise_perm_exc("/, Sys.Modify for modifying tag '$tag'");
+	}
+
+	return;
+    };
+
+    my $old_tags = {};
+    my $new_tags = {};
+    my $all_tags = {};
+
+    $all_tags->{$_} = $old_tags->{$_} += 1 for PVE::Tools::split_list($tagopt_old // '');
+    $all_tags->{$_} = $new_tags->{$_} += 1 for PVE::Tools::split_list($tagopt_new // '');
+
+    for my $tag (keys $all_tags->%*) {
+	next if ($new_tags->{$tag} // 0) == ($old_tags->{$tag} // 0);
+	$check_single_tag->($tag);
+    }
+}
+
 1;
-- 
2.30.2





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

* [pve-devel] [PATCH qemu-server v11 1/1] api: update: check for tags permissions with 'assert_tag_permissions'
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (5 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH guest-common v11 1/1] GuestHelpers: add tag related helpers Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH container v11 1/1] check_ct_modify_config_perm: " Dominik Csapak
                   ` (15 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

from GuestHelpers. This function checks all necessary permissions and
raises an exception if the user does not have the correct ones.

This is necessary for the new 'privileged' tags and 'user-tag-access'
permissions to work.

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

diff --git a/PVE/API2/Qemu.pm b/PVE/API2/Qemu.pm
index 30348e6..848a1bc 100644
--- a/PVE/API2/Qemu.pm
+++ b/PVE/API2/Qemu.pm
@@ -18,7 +18,7 @@ use PVE::Storage;
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::RESTHandler;
 use PVE::ReplicationConfig;
-use PVE::GuestHelpers;
+use PVE::GuestHelpers qw(assert_tag_permissions);
 use PVE::QemuConfig;
 use PVE::QemuServer;
 use PVE::QemuServer::Cloudinit;
@@ -539,7 +539,6 @@ my $generaloptions = {
     'startup' => 1,
     'tdf' => 1,
     'template' => 1,
-    'tags' => 1,
 };
 
 my $vmpoweroptions = {
@@ -609,6 +608,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+$/) {
@@ -1689,6 +1689,10 @@ my $update_vm_api  = sub {
 		    }
 		    PVE::QemuConfig->add_to_pending_delete($conf, $opt, $force);
 		    PVE::QemuConfig->write_config($vmid, $conf);
+		} elsif ($opt eq 'tags') {
+		    assert_tag_permissions($vmid, $val, '', $rpcenv, $authuser);
+		    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);
@@ -1749,6 +1753,9 @@ my $update_vm_api  = sub {
 			die "only root can modify '$opt' config for real devices\n";
 		    }
 		    $conf->{pending}->{$opt} = $param->{$opt};
+		} elsif ($opt eq 'tags') {
+		    assert_tag_permissions($vmid, $conf->{$opt}, $param->{$opt}, $rpcenv, $authuser);
+		    $conf->{pending}->{$opt} = $param->{$opt};
 		} else {
 		    $conf->{pending}->{$opt} = $param->{$opt};
 
-- 
2.30.2





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

* [pve-devel] [PATCH container v11 1/1] check_ct_modify_config_perm: check for tags permissions with 'assert_tag_permissions'
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (6 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH qemu-server v11 1/1] api: update: check for tags permissions with 'assert_tag_permissions' Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 01/13] api: /cluster/resources: add tags to returned properties Dominik Csapak
                   ` (14 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

from GuestHelpers. This function checks all necessary permissions and
raises an exception if the user does not have the correct ones.

This is necessary for the new 'privileged' tags and 'user-tag-access'
permissions to work.

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

diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index 4bbd739..3cedc9a 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -1336,6 +1336,10 @@ 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 $old = $oldconf->{$opt};
+	    my $new = $delete ? '' : $newconf->{$opt};
+	    PVE::GuestHelpers::assert_tag_permissions($vmid, $old, $new, $rpcenv, $authuser);
 	} else {
 	    $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Options']);
 	}
-- 
2.30.2





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

* [pve-devel] [PATCH manager v11 01/13] api: /cluster/resources: add tags to returned properties
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (7 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH container v11 1/1] check_ct_modify_config_perm: " Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 02/13] api: allow all users to (partially) read datacenter.cfg Dominik Csapak
                   ` (13 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 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 dc575d85b..3ca85caa4 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -376,7 +376,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) {
 
@@ -408,8 +409,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] 25+ messages in thread

* [pve-devel] [PATCH manager v11 02/13] api: allow all users to (partially) read datacenter.cfg
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (8 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 01/13] api: /cluster/resources: add tags to returned properties Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 03/13] ui: save ui options from /cluster/options instead of version Dominik Csapak
                   ` (12 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

it contains most ui relevant options, like the console preference and tag-style
so allow these for users without 'Sys.Audit' on '/'
(unchanged for all others)

we also add the list of allowed tags. while not strictly a datacenter
config, it's derived from the current users privileges and the
datacenter config.

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

diff --git a/PVE/API2.pm b/PVE/API2.pm
index a42561604..6703b941a 100644
--- a/PVE/API2.pm
+++ b/PVE/API2.pm
@@ -5,6 +5,7 @@ use warnings;
 
 use PVE::pvecfg;
 use PVE::DataCenterConfig;
+use PVE::GuestHelpers;
 use PVE::RESTHandler;
 use PVE::JSONSchema;
 
@@ -118,6 +119,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};
@@ -129,5 +131,4 @@ __PACKAGE__->register_method ({
 
 	return $res;
     }});
-
 1;
diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index 3ca85caa4..a06dc83a2 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -10,6 +10,7 @@ use PVE::Cluster qw(cfs_register_file cfs_lock_file cfs_read_file cfs_write_file
 use PVE::DataCenterConfig;
 use PVE::Exception qw(raise_param_exc);
 use PVE::Firewall;
+use PVE::GuestHelpers;
 use PVE::HA::Config;
 use PVE::HA::Env::PVE2;
 use PVE::INotify;
@@ -542,8 +543,9 @@ __PACKAGE__->register_method({
     name => 'get_options',
     path => 'options',
     method => 'GET',
-    description => "Get datacenter options.",
+    description => "Get datacenter options. Without 'Sys.Audit' on '/' not all options are returned.",
     permissions => {
+	user => 'all',
 	check => ['perm', '/', [ 'Sys.Audit' ]],
     },
     parameters => {
@@ -557,7 +559,25 @@ __PACKAGE__->register_method({
     code => sub {
 	my ($param) = @_;
 
-	return PVE::Cluster::cfs_read_file('datacenter.cfg');
+	my $res = {};
+
+	my $rpcenv = PVE::RPCEnvironment::get();
+	my $authuser = $rpcenv->get_user();
+
+	my $datacenter_config = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
+
+	if ($rpcenv->check($authuser, '/', ['Sys.Audit'], 1)) {
+	    $res = $datacenter_config;
+	} else {
+	    for my $k (qw(console tag-style)) {
+		$res->{$k} = $datacenter_config->{$k} if exists $datacenter_config->{$k};
+	    }
+	}
+
+	my $tags = PVE::GuestHelpers::get_allowed_tags($rpcenv, $authuser);
+	$res->{'allowed-tags'} = [sort keys $tags->%*];
+
+	return $res;
     }});
 
 __PACKAGE__->register_method({
-- 
2.30.2





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

* [pve-devel] [PATCH manager v11 03/13] ui: save ui options from /cluster/options instead of version
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (9 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 02/13] api: allow all users to (partially) read datacenter.cfg Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 04/13] ui: parse and save tag infos from /cluster/options Dominik Csapak
                   ` (11 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

/cluster/options is now the go to place for getting these options
(until we have more options unrelated to the datacenter.cfg)

Also 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         | 13 ++++++++++++-
 www/manager6/Workspace.js     |  2 ++
 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 adcf082ff..6ce22ded5 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) {
@@ -1854,6 +1854,17 @@ Ext.define('PVE.Utils', {
     },
 
     notesTemplateVars: ['cluster', 'guestname', 'node', 'vmid'],
+
+    updateUIOptions: function() {
+	Proxmox.Utils.API2Request({
+	    url: '/cluster/options',
+	    method: 'GET',
+	    success: function(response) {
+		PVE.UIOptions = {};
+		PVE.UIOptions.console = response?.result?.data?.console;
+	    },
+	});
+    },
 },
 
     singleton: true,
diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js
index 2bb502e0c..a7423508e 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -158,6 +158,8 @@ Ext.define('PVE.StdWorkspace', {
 		},
 	    });
 
+	    PVE.Utils.updateUIOptions();
+
 	    Proxmox.Utils.API2Request({
 		url: '/cluster/sdn',
 		method: 'GET',
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index 5a2be182e..ff96351d5 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] 25+ messages in thread

* [pve-devel] [PATCH manager v11 04/13] ui: parse and save tag infos from /cluster/options
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (10 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 03/13] ui: save ui options from /cluster/options instead of version Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 05/13] ui: add form/TagColorGrid Dominik Csapak
                   ` (10 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

stores the color-map 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

also saves the list of 'allowed-tags' into PVE.Utils

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/manager6/Utils.js | 60 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 58 insertions(+), 2 deletions(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 6ce22ded5..7137a3a25 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1860,11 +1860,67 @@ Ext.define('PVE.Utils', {
 	    url: '/cluster/options',
 	    method: 'GET',
 	    success: function(response) {
-		PVE.UIOptions = {};
-		PVE.UIOptions.console = response?.result?.data?.console;
+		PVE.UIOptions = {
+		    'allowed-tags': [],
+		};
+		for (const option of ['allowed-tags', 'console', 'tag-style']) {
+		    PVE.UIOptions[option] = response?.result?.data?.[option];
+		}
+
+		PVE.Utils.updateTagList(PVE.UIOptions['allowed-tags']);
+		PVE.Utils.updateTagSettings(PVE.UIOptions?.['tag-style']);
 	    },
 	});
     },
+
+    tagList: [],
+
+    updateTagList: function(tags) {
+	PVE.Utils.tagList = [...new Set([...tags])].sort();
+    },
+
+    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);
+    },
+
+    updateTagSettings: function(style) {
+	let overrides = style?.['color-map'];
+	PVE.Utils.updateTagOverrides(PVE.Utils.parseTagOverrides(overrides ?? ""));
+
+	let shape = style?.shape ?? 'circle';
+	if (shape === '__default__') {
+	    style = 'circle';
+	}
+
+	Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${shape}`);
+	PVE.data.ResourceStore.fireEvent('load');
+	Ext.GlobalEvents.fireEvent('loadedUiOptions');
+    },
 },
 
     singleton: true,
-- 
2.30.2





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

* [pve-devel] [PATCH manager v11 05/13] ui: add form/TagColorGrid
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (11 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 04/13] ui: parse and save tag infos from /cluster/options Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 06/13] ui: add PVE.form.ListField Dominik Csapak
                   ` (9 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 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 dadb84a97..f7d0c4201 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 2802cbacd..572aa9392 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 7137a3a25..2ff0fbd81 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1921,6 +1921,8 @@ Ext.define('PVE.Utils', {
 	PVE.data.ResourceStore.fireEvent('load');
 	Ext.GlobalEvents.fireEvent('loadedUiOptions');
     },
+
+    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 000000000..3ad8e07f0
--- /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] 25+ messages in thread

* [pve-devel] [PATCH manager v11 06/13] ui: add PVE.form.ListField
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (12 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 05/13] ui: add form/TagColorGrid Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 07/13] ui: dc/OptionView: add editors for tag settings Dominik Csapak
                   ` (8 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

a field which contains just a list of textfields, e.g. useful for a list
of simple tags

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

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 572aa9392..83c2effcf 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -74,6 +74,7 @@ JSSRC= 							\
 	form/ViewSelector.js				\
 	form/iScsiProviderSelector.js			\
 	form/TagColorGrid.js				\
+	form/ListField.js				\
 	grid/BackupView.js				\
 	grid/FirewallAliases.js				\
 	grid/FirewallOptions.js				\
diff --git a/www/manager6/form/ListField.js b/www/manager6/form/ListField.js
new file mode 100644
index 000000000..faa8e168b
--- /dev/null
+++ b/www/manager6/form/ListField.js
@@ -0,0 +1,165 @@
+Ext.define('PVE.form.ListField', {
+    extend: 'Ext.grid.Panel',
+    alias: 'widget.pveListField',
+
+    mixins: [
+	'Ext.form.field.Field',
+    ],
+
+    // override for column header
+    fieldTitle: gettext('Item'),
+
+    // will be applied to the textfields
+    maskRe: undefined,
+
+    allowBlank: true,
+    selectAll: false,
+    isFormField: true,
+    deleteEmpty: false,
+    selModel: 'checkboxmodel',
+
+    config: {
+	deleteEmpty: false,
+    },
+
+    viewConfig: {
+	deferEmptyText: false,
+    },
+
+    setValue: function(list) {
+	let me = this;
+	list = Ext.isArray(list) ? list : (list ?? '').split(';');
+	if (list.length > 0) {
+	    me.getStore().setData(list.map(item => ({ item })));
+	} else {
+	    me.getStore().removeAll();
+	}
+	me.checkChange();
+	return me;
+    },
+
+    getValue: function() {
+	let me = this;
+	let values = [];
+	me.getStore().each((rec) => {
+	    if (rec.data.item) {
+		values.push(rec.data.item);
+	    }
+	});
+	return values.join(';');
+    },
+
+    getErrors: function(value) {
+	let me = this;
+	let empty = false;
+	me.getStore().each((rec) => {
+	    if (!rec.data.item) {
+		empty = true;
+	    }
+	});
+	if (empty) {
+	    return [gettext('Tag must not be empty.')];
+	}
+	return [];
+    },
+
+    // override framework function to implement deleteEmpty behaviour
+    getSubmitData: function() {
+	let me = this,
+	    data = null,
+	    val;
+	if (!me.disabled && me.submitValue) {
+	    val = me.getValue();
+	    if (val !== null && val !== '') {
+		data = {};
+		data[me.getName()] = val;
+	    } else if (me.getDeleteEmpty()) {
+		data = {};
+		data.delete = me.getName();
+	    }
+	}
+	return data;
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	addLine: function() {
+	    let me = this;
+	    me.getView().getStore().add({
+		item: '',
+	    });
+	},
+
+	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();
+	},
+
+	itemChange: function(field, newValue) {
+	    let rec = field.getWidgetRecord();
+	    if (!rec) {
+		return;
+	    }
+	    let column = field.getWidgetColumn();
+	    rec.set(column.dataIndex, newValue);
+	    field.up('pveListField').checkChange();
+	},
+    },
+
+    tbar: [
+	{
+	    text: gettext('Add'),
+	    handler: 'addLine',
+	},
+	{
+	    xtype: 'proxmoxButton',
+	    text: gettext('Remove'),
+	    handler: 'removeSelection',
+	    disabled: true,
+	},
+    ],
+
+    store: {
+	listeners: {
+	    update: function() {
+		this.commitChanges();
+	    },
+	},
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	me.columns = [
+	    {
+		header: me.fieldTtitle,
+		dataIndex: 'item',
+		xtype: 'widgetcolumn',
+		widget: {
+		    xtype: 'textfield',
+		    isFormField: false,
+		    maskRe: me.maskRe,
+		    allowBlank: false,
+		    queryMode: 'local',
+		    listeners: {
+			change: 'itemChange',
+		    },
+		},
+		flex: 1,
+	    },
+	];
+
+	me.callParent();
+	me.initField();
+    },
+});
-- 
2.30.2





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

* [pve-devel] [PATCH manager v11 07/13] ui: dc/OptionView: add editors for tag settings
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (13 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 06/13] ui: add PVE.form.ListField Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 08/13] ui: add form/Tag Dominik Csapak
                   ` (7 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 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/Makefile                 |   2 +
 www/manager6/Utils.js                 |  20 +++++
 www/manager6/dc/OptionView.js         | 121 ++++++++++++++++++++++++++
 www/manager6/dc/RegisteredTagsEdit.js |  70 +++++++++++++++
 www/manager6/dc/UserTagAccessEdit.js  |  90 +++++++++++++++++++
 5 files changed, 303 insertions(+)
 create mode 100644 www/manager6/dc/RegisteredTagsEdit.js
 create mode 100644 www/manager6/dc/UserTagAccessEdit.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 83c2effcf..2cbb65eb5 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -160,6 +160,8 @@ JSSRC= 							\
 	dc/UserEdit.js					\
 	dc/UserView.js					\
 	dc/MetricServerView.js				\
+	dc/UserTagAccessEdit.js				\
+	dc/RegisteredTagsEdit.js			\
 	lxc/CmdMenu.js					\
 	lxc/Config.js					\
 	lxc/CreateWizard.js				\
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 2ff0fbd81..940752469 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1922,6 +1922,26 @@ Ext.define('PVE.Utils', {
 	Ext.GlobalEvents.fireEvent('loadedUiOptions');
     },
 
+    tagTreeStyles: {
+	'__default__': `${Proxmox.Utils.defaultText} (${gettext('Cirlce')})`,
+	'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 ff96351d5..be9e2abb2 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,123 @@ 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?.['color-map']);
+		let shape = value.shape;
+		let shapeText = PVE.Utils.tagTreeStyles[shape ?? '__default__'];
+		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'),
+		onlineHelp: 'datacenter_configuration_file',
+		fieldDefaults: {
+		    labelWidth: 100,
+		},
+		url: '/api2/extjs/cluster/options',
+		items: [
+		    {
+			xtype: 'inputpanel',
+			setValues: function(values) {
+			    if (values === undefined) {
+				return undefined;
+			    }
+			    values = values?.['tag-style'] ?? {};
+			    values.shape = values.shape || '__default__';
+			    values.colors = values['color-map'];
+			    return Proxmox.panel.InputPanel.prototype.setValues.call(this, values);
+			},
+			onGetValues: function(values) {
+			    let style = {};
+			    if (values.colors) {
+				style['color-map'] = values.colors;
+			    }
+			    if (values.shape) {
+				style.shape = values.shape;
+			    }
+			    let value = PVE.Parser.printPropertyString(style);
+			    if (value === '') {
+				return {
+				    'delete': 'tag-style',
+				};
+			    }
+			    return {
+				'tag-style': value,
+			    };
+			},
+			items: [
+			    {
+				name: '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.rows['user-tag-access'] = {
+	    required: true,
+	    renderer: (value) => {
+		if (value === undefined) {
+		    return Ext.String.format(gettext('Mode: {0}'), 'free');
+		}
+		let mode = value?.['user-allow'] ?? 'free';
+		let list = value?.['user-allow-list'].join(',');
+		let modeTxt = Ext.String.format(gettext('Mode {0}'), mode);
+		let overrides = PVE.Utils.tagOverrides;
+		let tags = PVE.Utils.renderTags(list, overrides);
+
+		return `${modeTxt}, ${gettext('Pre-defined:')} ${tags}`;
+	    },
+	    header: gettext('User Tag Access'),
+	    editor: {
+		xtype: 'pveUserTagAccessEdit',
+	    },
+	};
+
+	me.rows['registered-tags'] = {
+	    required: true,
+	    renderer: (value) => {
+		if (value === undefined) {
+		    return gettext('No Registered Tags');
+		}
+		let overrides = PVE.Utils.tagOverrides;
+		return PVE.Utils.renderTags(value.join(','), overrides);
+	    },
+	    header: gettext('Registered Tags'),
+	    editor: {
+		xtype: 'pveRegisteredTagEdit',
+	    },
+	};
 
 	me.selModel = Ext.create('Ext.selection.RowModel', {});
 
@@ -347,6 +465,9 @@ Ext.define('PVE.dc.OptionView', {
 	    if (rec.data.value === '__default__') {
 		delete PVE.UIOptions.console;
 	    }
+
+	    PVE.UIOptions['tag-style'] = store.getById('tag-style')?.data?.value;
+	    PVE.Utils.updateTagSettings(PVE.UIOptions['tag-style']);
 	});
 
 	me.on('activate', me.rstore.startUpdate);
diff --git a/www/manager6/dc/RegisteredTagsEdit.js b/www/manager6/dc/RegisteredTagsEdit.js
new file mode 100644
index 000000000..92e076e0a
--- /dev/null
+++ b/www/manager6/dc/RegisteredTagsEdit.js
@@ -0,0 +1,70 @@
+Ext.define('PVE.dc.RegisteredTagsEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pveRegisteredTagEdit',
+
+    subject: gettext('Registered Tags'),
+    onlineHelp: 'datacenter_configuration_file',
+
+    url: '/api2/extjs/cluster/options',
+
+    hintText: gettext('NOTE: The following tags are also defined in the user allow list.'),
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	tagChange: function(field, value) {
+	    let me = this;
+	    let view = me.getView();
+	    let also_allowed = [];
+	    value = Ext.isArray(value) ? value : value.split(';');
+	    value.forEach(tag => {
+		if (view.allowed_tags.indexOf(tag) !== -1) {
+		    also_allowed.push(tag);
+		}
+	    });
+	    let hint_field = me.lookup('hintField');
+	    hint_field.setVisible(also_allowed.length > 0);
+	    if (also_allowed.length > 0) {
+		hint_field.setValue(`${view.hintText} ${also_allowed.join(', ')}`);
+	    }
+	},
+    },
+
+    items: [
+	{
+	    xtype: 'inputpanel',
+	    setValues: function(values) {
+		let allowed_tags = values?.['user-tag-access']?.['user-allow-list'] ?? [];
+		this.up('pveRegisteredTagEdit').allowed_tags = allowed_tags;
+
+		let tags = values?.['registered-tags'] ?? '';
+		return Proxmox.panel.InputPanel.prototype.setValues.call(this, { tags });
+	    },
+	    onGetValues: function(values) {
+		return {
+		    'registered-tags': values,
+		};
+	    },
+	    items: [
+		{
+		    name: 'tags',
+		    xtype: 'pveListField',
+		    emptyText: gettext('No Tags defined'),
+		    fieldTitle: gettext('Tag'),
+		    maskRe: PVE.Utils.tagCharRegex,
+		    height: 200,
+		    scrollable: true,
+		    listeners: {
+			change: 'tagChange',
+		    },
+		},
+		{
+		    hidden: true,
+		    xtype: 'displayfield',
+		    reference: 'hintField',
+		    userCls: 'pmx-hint',
+		},
+	    ],
+	},
+    ],
+});
diff --git a/www/manager6/dc/UserTagAccessEdit.js b/www/manager6/dc/UserTagAccessEdit.js
new file mode 100644
index 000000000..701d1de4f
--- /dev/null
+++ b/www/manager6/dc/UserTagAccessEdit.js
@@ -0,0 +1,90 @@
+Ext.define('PVE.dc.UserTagAccessEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'widget.pveUserTagAccessEdit',
+
+    subject: gettext('User Tag Access'),
+    onlineHelp: 'datacenter_configuration_file',
+
+    url: '/api2/extjs/cluster/options',
+
+    hintText: gettext('NOTE: The following tags are also defined as registered tags.'),
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	tagChange: function(field, value) {
+	    let me = this;
+	    let view = me.getView();
+	    let also_registered = [];
+	    value = Ext.isArray(value) ? value : value.split(';');
+	    value.forEach(tag => {
+		if (view.registered_tags.indexOf(tag) !== -1) {
+		    also_registered.push(tag);
+		}
+	    });
+	    let hint_field = me.lookup('hintField');
+	    hint_field.setVisible(also_registered.length > 0);
+	    if (also_registered.length > 0) {
+		hint_field.setValue(`${view.hintText} ${also_registered.join(', ')}`);
+	    }
+	},
+    },
+
+    items: [
+	{
+	    xtype: 'inputpanel',
+	    setValues: function(values) {
+		this.up('pveUserTagAccessEdit').registered_tags = values?.['registered-tags'] ?? [];
+		let data = values?.['user-tag-access'] ?? {};
+		return Proxmox.panel.InputPanel.prototype.setValues.call(this, data);
+	    },
+	    onGetValues: function(values) {
+		if (values === undefined || Object.keys(values).length === 0) {
+		    return { 'delete': name };
+		}
+		return {
+		    'user-tag-access': PVE.Parser.printPropertyString(values),
+		};
+	    },
+	    items: [
+		{
+		    name: 'user-allow',
+		    fieldLabel: gettext('Mode'),
+		    xtype: 'proxmoxKVComboBox',
+		    deleteEmpty: false,
+		    value: '__default__',
+		    comboItems: [
+			['__default__', Proxmox.Utils.defaultText + ' (free)'],
+			['free', 'free'],
+			['existing', 'existing'],
+			['list', 'list'],
+			['none', 'none'],
+		    ],
+		    defaultValue: '__default__',
+		},
+		{
+		    xtype: 'displayfield',
+		    fieldLabel: gettext('Predefined Tags'),
+		},
+		{
+		    name: 'user-allow-list',
+		    xtype: 'pveListField',
+		    emptyText: gettext('No Tags defined'),
+		    fieldTitle: gettext('Tag'),
+		    maskRe: PVE.Utils.tagCharRegex,
+		    height: 200,
+		    scrollable: true,
+		    listeners: {
+			change: 'tagChange',
+		    },
+		},
+		{
+		    hidden: true,
+		    xtype: 'displayfield',
+		    reference: 'hintField',
+		    userCls: 'pmx-hint',
+		},
+	    ],
+	},
+    ],
+});
-- 
2.30.2





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

* [pve-devel] [PATCH manager v11 08/13] ui: add form/Tag
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (14 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 07/13] ui: dc/OptionView: add editors for tag settings Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 09/13] ui: add form/TagEdit.js Dominik Csapak
                   ` (6 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 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.

this is a basic ext component, with 'i' tags for the icons (handle and
add/remove button) and a span (for the tag text)

shows the tag by default, and if put in editable mode, makes the
span editable. when actually starting the edit, shows a picker
with available tags to select from

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/css/ext6-pve.css     |  32 ++++++
 www/manager6/Makefile    |   1 +
 www/manager6/form/Tag.js | 232 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 265 insertions(+)
 create mode 100644 www/manager6/form/Tag.js

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index f7d0c4201..daaffa6ec 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -656,3 +656,35 @@ table.osds td:first-of-type {
     padding-top: 0px;
     padding-bottom: 0px;
 }
+
+.pve-edit-tag > i {
+    cursor: pointer;
+    font-size: 14px;
+}
+
+.pve-edit-tag > i.handle {
+    padding-right: 5px;
+    cursor: grab;
+}
+
+.pve-edit-tag > i.action {
+    padding-left: 5px;
+}
+
+.pve-edit-tag.normal > i {
+    display: none;
+}
+
+.pve-edit-tag.editable span,
+.pve-edit-tag.inEdit span {
+    background-color: #ffffff;
+    border: 1px solid #a8a8a8;
+    color: #000;
+    padding-left: 2px;
+    padding-right: 2px;
+    min-width: 2em;
+}
+
+.pve-edit-tag.inEdit span {
+    border: 1px solid #000;
+}
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 2cbb65eb5..d64a4ba27 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -75,6 +75,7 @@ JSSRC= 							\
 	form/iScsiProviderSelector.js			\
 	form/TagColorGrid.js				\
 	form/ListField.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 000000000..9acedb527
--- /dev/null
+++ b/www/manager6/form/Tag.js
@@ -0,0 +1,232 @@
+Ext.define('Proxmox.form.Tag', {
+    extend: 'Ext.Component',
+    alias: 'widget.pveTag',
+
+    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();
+    },
+
+    destroy: function() {
+	let me = this;
+	if (me.picker) {
+	    Ext.destroy(me.picker);
+	}
+	me.callParent();
+    },
+});
-- 
2.30.2





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

* [pve-devel] [PATCH manager v11 09/13] ui: add form/TagEdit.js
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (15 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 08/13] ui: add form/Tag Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 10/13] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
                   ` (5 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 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 'pveTag' class, but overwrite some of
its behaviour and css classes so that it properly adds tags

also handles the drag/drop feature for the tags in the list

when done with editing (by clicking the checkmark), sends a 'change'
event with the new tags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/css/ext6-pve.css         |  23 ++-
 www/manager6/Makefile        |   1 +
 www/manager6/form/TagEdit.js | 325 +++++++++++++++++++++++++++++++++++
 3 files changed, 345 insertions(+), 4 deletions(-)
 create mode 100644 www/manager6/form/TagEdit.js

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index daaffa6ec..4fc83a878 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -657,7 +657,8 @@ table.osds td:first-of-type {
     padding-bottom: 0px;
 }
 
-.pve-edit-tag > i {
+.pve-edit-tag > i,
+.pve-add-tag > i {
     cursor: pointer;
     font-size: 14px;
 }
@@ -667,7 +668,8 @@ table.osds td:first-of-type {
     cursor: grab;
 }
 
-.pve-edit-tag > i.action {
+.pve-edit-tag > i.action,
+.pve-add-tag > i.action {
     padding-left: 5px;
 }
 
@@ -676,7 +678,9 @@ table.osds td:first-of-type {
 }
 
 .pve-edit-tag.editable span,
-.pve-edit-tag.inEdit span {
+.pve-edit-tag.inEdit span,
+.pve-add-tag.editable span,
+.pve-add-tag.inEdit span {
     background-color: #ffffff;
     border: 1px solid #a8a8a8;
     color: #000;
@@ -685,6 +689,17 @@ table.osds td:first-of-type {
     min-width: 2em;
 }
 
-.pve-edit-tag.inEdit span {
+.pve-edit-tag.inEdit span,
+.pve-add-tag.inEdit span {
     border: 1px solid #000;
 }
+
+.pve-add-tag {
+    background-color: #d5d5d5 ! important;
+    color: #000000 ! important;
+}
+
+.pve-tag-inline-button {
+    cursor: pointer;
+    padding-left: 2px;
+}
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index d64a4ba27..9786337bb 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -76,6 +76,7 @@ JSSRC= 							\
 	form/TagColorGrid.js				\
 	form/ListField.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 000000000..a6e14349e
--- /dev/null
+++ b/www/manager6/form/TagEdit.js
@@ -0,0 +1,325 @@
+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() === 'pveTag') {
+		    func(field);
+		}
+		return true;
+	    });
+	},
+
+	toggleEdit: function(cancel) {
+	    let me = this;
+	    let vm = me.getViewModel();
+	    let editMode = !vm.get('editMode');
+	    vm.set('editMode', editMode);
+
+	    // get a current tag list for editing
+	    if (editMode) {
+		PVE.Utils.updateUIOptions();
+	    }
+
+	    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: 'pveTag',
+		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);
+	    }
+
+	    me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
+		me.loadTags(me.oldTags, true); // refresh tag colors
+	    });
+	},
+    },
+
+    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: 'pveTag',
+	    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] 25+ messages in thread

* [pve-devel] [PATCH manager v11 10/13] ui: {lxc, qemu}/Config: show Tags and make them editable
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (16 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 09/13] ui: add form/TagEdit.js Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 11/13] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
                   ` (4 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 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 93f385db4..9b3017add 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 9fe933dfc..2cd6d8567 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] 25+ messages in thread

* [pve-devel] [PATCH manager v11 11/13] ui: tree/ResourceTree: show Tags in tree
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (17 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 10/13] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 12/13] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
                   ` (3 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

and update the treenodes when the tags change.
since we change the vm node text (which we pass through to the config
panel), we have to change how we generate the text there slightly
(otherwise that would include the rendered tags)

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

diff --git a/www/manager6/data/ResourceStore.js b/www/manager6/data/ResourceStore.js
index c7b723060..b18f7dd8d 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/lxc/Config.js b/www/manager6/lxc/Config.js
index 9b3017add..f33390513 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 2cd6d8567..5c8fa620d 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 be90d4f7a..5c92d4128 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;
     },
 
@@ -226,6 +230,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 +269,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;
-- 
2.30.2





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

* [pve-devel] [PATCH manager v11 12/13] ui: add tags to ResourceGrid and GlobalSearchField
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (18 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 11/13] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 13/13] ui: implement tag ordering from datacenter.cfg Dominik Csapak
                   ` (2 subsequent siblings)
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 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 b18f7dd8d..ed1f4699e 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 267a480d7..8e815d4f5 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 29906a376..9376bcc26 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] 25+ messages in thread

* [pve-devel] [PATCH manager v11 13/13] ui: implement tag ordering from datacenter.cfg
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (19 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 12/13] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
@ 2022-11-16 15:48 ` Dominik Csapak
  2022-11-17 10:18 ` [pve-devel] partially-applied: [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Thomas Lamprecht
  2022-11-17 17:22 ` [pve-devel] applied: " Thomas Lamprecht
  22 siblings, 0 replies; 25+ messages in thread
From: Dominik Csapak @ 2022-11-16 15:48 UTC (permalink / raw)
  To: pve-devel

datacenter.cfg returns an 'ordering' option. parse that and
use it to order the tags when viewing. default is alphabetical.

with alphabetical ordering, drag & drop when editing is disabled and the
tags will be inserted at the right place. when saving, the sorted
order will be written into the config

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 www/css/ext6-pve.css          |  5 +++++
 www/manager6/Utils.js         | 13 +++++++++++++
 www/manager6/dc/OptionView.js | 14 ++++++++++++++
 www/manager6/form/TagEdit.js  | 13 ++++++++++++-
 4 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index 4fc83a878..b8c713c48 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -668,6 +668,11 @@ table.osds td:first-of-type {
     cursor: grab;
 }
 
+.hide-handles .pve-edit-tag > i.handle {
+    display: none;
+    padding-right: 0px;
+}
+
 .pve-edit-tag > i.action,
 .pve-add-tag > i.action {
     padding-left: 5px;
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 940752469..8484372f2 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1930,10 +1930,19 @@ Ext.define('PVE.Utils', {
 	'none': Proxmox.Utils.NoneText,
     },
 
+    tagOrderOptions: {
+	'__default__': `${Proxmox.Utils.defaultText} (${gettext('Alphabetical')})`,
+	'config': gettext('Configuration'),
+	'alphabetical': gettext('Alphabetical'),
+    },
+
     renderTags: function(tagstext, overrides) {
 	let text = '';
 	if (tagstext) {
 	    let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
+	    if (PVE.Utils.shouldSortTags()) {
+		tags = tags.sort();
+	    }
 	    text += ' ';
 	    tags.forEach((tag) => {
 		text += Proxmox.Utils.getTagElement(tag, overrides);
@@ -1942,6 +1951,10 @@ Ext.define('PVE.Utils', {
 	return text;
     },
 
+    shouldSortTags: function() {
+	return !(PVE.UIOptions?.['tag-style']?.ordering === 'config');
+    },
+
     tagCharRegex: /^[a-z0-9+_.-]$/i,
 },
 
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index be9e2abb2..aeab024e4 100644
--- a/www/manager6/dc/OptionView.js
+++ b/www/manager6/dc/OptionView.js
@@ -323,6 +323,8 @@ Ext.define('PVE.dc.OptionView', {
 		let shape = value.shape;
 		let shapeText = PVE.Utils.tagTreeStyles[shape ?? '__default__'];
 		let txt = Ext.String.format(gettext("Tree Shape: {0}"), shapeText);
+		let orderText = PVE.Utils.tagOrderOptions[value.ordering ?? '__default__'];
+		txt += `, ${Ext.String.format(gettext("Ordering: {0}"), orderText)}`;
 		if (Object.keys(colors).length > 0) {
 		    txt += ', ';
 		}
@@ -361,6 +363,9 @@ Ext.define('PVE.dc.OptionView', {
 			    if (values.shape) {
 				style.shape = values.shape;
 			    }
+			    if (values.ordering) {
+				style.ordering = values.ordering;
+			    }
 			    let value = PVE.Parser.printPropertyString(style);
 			    if (value === '') {
 				return {
@@ -380,6 +385,15 @@ Ext.define('PVE.dc.OptionView', {
 				defaultValue: '__default__',
 				deleteEmpty: true,
 			    },
+			    {
+				name: 'ordering',
+				xtype: 'proxmoxKVComboBox',
+				fieldLabel: gettext('Ordering'),
+				comboItems: Object.entries(PVE.Utils.tagOrderOptions),
+				defaultValue: '__default__',
+				value: '__default__',
+				deleteEmpty: true,
+			    },
 			    {
 				xtype: 'displayfield',
 				fieldLabel: gettext('Color Overrides'),
diff --git a/www/manager6/form/TagEdit.js b/www/manager6/form/TagEdit.js
index a6e14349e..6325d39df 100644
--- a/www/manager6/form/TagEdit.js
+++ b/www/manager6/form/TagEdit.js
@@ -37,6 +37,8 @@ Ext.define('PVE.panel.TagEditContainer', {
 	onRender: function(v) {
 	    let me = this;
 	    let view = me.getView();
+	    view.toggleCls('hide-handles', PVE.Utils.shouldSortTags());
+
 	    view.dragzone = Ext.create('Ext.dd.DragZone', v.getEl(), {
 		getDragData: function(e) {
 		    let source = e.getTarget('.handle');
@@ -168,6 +170,14 @@ Ext.define('PVE.panel.TagEditContainer', {
 	    let view = me.getView();
 	    let vm = me.getViewModel();
 	    let index = view.items.indexOf(me.lookup('addTagBtn'));
+	    if (PVE.Utils.shouldSortTags()) {
+		index = view.items.findIndexBy(tagField => {
+		    if (tagField.reference === 'addTagBtn') {
+			return true;
+		    }
+		    return tagField.tag >= tag;
+		}, 1);
+	    }
 	    view.insert(index, {
 		xtype: 'pveTag',
 		tag,
@@ -226,7 +236,8 @@ Ext.define('PVE.panel.TagEditContainer', {
 	    }
 
 	    me.mon(Ext.GlobalEvents, 'loadedUiOptions', () => {
-		me.loadTags(me.oldTags, true); // refresh tag colors
+		view.toggleCls('hide-handles', PVE.Utils.shouldSortTags());
+		me.loadTags(me.oldTags, true); // refresh tag colors and order
 	    });
 	},
     },
-- 
2.30.2





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

* [pve-devel] partially-applied: [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (20 preceding siblings ...)
  2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 13/13] ui: implement tag ordering from datacenter.cfg Dominik Csapak
@ 2022-11-17 10:18 ` Thomas Lamprecht
  2022-11-17 17:22 ` [pve-devel] applied: " Thomas Lamprecht
  22 siblings, 0 replies; 25+ messages in thread
From: Thomas Lamprecht @ 2022-11-17 10:18 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

Am 16/11/2022 um 16:47 schrieb Dominik Csapak:
> pve-cluster:
> 
> Dominik Csapak (5):
>   add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
>   Cluster: add get_guest_config_properties
>   datacenter.cfg: add option for tag-style
>   datacenter.cfg: add tag rights control to the datacenter config
>   datacenter.cfg: add 'ordering' to 'tag-style' config
> 

applied these with commit messages reworked to current patch revision (e.g.,
the allowed tags helper isn't in cluster anymore) and easier to read.





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

* [pve-devel] applied: [PATCH guest-common v11 1/1] GuestHelpers: add tag related helpers
  2022-11-16 15:48 ` [pve-devel] [PATCH guest-common v11 1/1] GuestHelpers: add tag related helpers Dominik Csapak
@ 2022-11-17 12:12   ` Thomas Lamprecht
  0 siblings, 0 replies; 25+ messages in thread
From: Thomas Lamprecht @ 2022-11-17 12:12 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

Am 16/11/2022 um 16:48 schrieb Dominik Csapak:
> 'get_allowed_tags':
> returns the allowed tags for the given user
> 
> 'assert_tag_permissions'
> helper to check permissions for tag setting/updating/deleting
> for both container and qemu-server
> 
> gets the list of allowed tags from the DataCenterConfig and the current
> user permissions, and checks for each tag that is added/removed if
> the user has permissions to modify it
> 
> 'normal' tags require 'VM.Config.Options' on '/vms/<vmid>', but not
> allowed tags (either limited with 'user-tag-access' or
> 'privileged-tags' in the datacenter.cfg) requrie 'Sys.Modify' on '/'
> 
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
>  debian/control          |  3 +-
>  src/PVE/GuestHelpers.pm | 99 ++++++++++++++++++++++++++++++++++++++++-
>  2 files changed, 100 insertions(+), 2 deletions(-)
> 
>

applied, thanks!




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

* [pve-devel] applied: [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui
  2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
                   ` (21 preceding siblings ...)
  2022-11-17 10:18 ` [pve-devel] partially-applied: [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Thomas Lamprecht
@ 2022-11-17 17:22 ` Thomas Lamprecht
  22 siblings, 0 replies; 25+ messages in thread
From: Thomas Lamprecht @ 2022-11-17 17:22 UTC (permalink / raw)
  To: Proxmox VE development discussion, Dominik Csapak

Am 16/11/2022 um 16:47 schrieb Dominik Csapak:
> pve-manager:
> 
> Dominik Csapak (13):
>   api: /cluster/resources: add tags to returned properties
>   api: allow all users to (partially) read datacenter.cfg
>   ui: save ui options from /cluster/options instead of version
>   ui: parse and save tag infos from /cluster/options
>   ui: add form/TagColorGrid
>   ui: add PVE.form.ListField
>   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
>   ui: implement tag ordering from datacenter.cfg
> 
>  PVE/API2.pm                            |   3 +-
>  PVE/API2/Cluster.pm                    |  33 ++-
>  www/css/ext6-pve.css                   |  57 ++++
>  www/manager6/Makefile                  |   6 +
>  www/manager6/Utils.js                  | 104 ++++++-
>  www/manager6/Workspace.js              |   2 +
>  www/manager6/data/ResourceStore.js     |   7 +
>  www/manager6/dc/OptionView.js          | 139 +++++++++-
>  www/manager6/dc/RegisteredTagsEdit.js  |  70 +++++
>  www/manager6/dc/UserTagAccessEdit.js   |  90 +++++++
>  www/manager6/form/GlobalSearchField.js |  20 +-
>  www/manager6/form/ListField.js         | 165 ++++++++++++
>  www/manager6/form/Tag.js               | 232 ++++++++++++++++
>  www/manager6/form/TagColorGrid.js      | 357 +++++++++++++++++++++++++
>  www/manager6/form/TagEdit.js           | 336 +++++++++++++++++++++++
>  www/manager6/grid/ResourceGrid.js      |   1 +
>  www/manager6/lxc/Config.js             |  36 ++-
>  www/manager6/qemu/Config.js            |  35 ++-
>  www/manager6/tree/ResourceTree.js      |  10 +-
>  19 files changed, 1682 insertions(+), 21 deletions(-)
>  create mode 100644 www/manager6/dc/RegisteredTagsEdit.js
>  create mode 100644 www/manager6/dc/UserTagAccessEdit.js
>  create mode 100644 www/manager6/form/ListField.js
>  create mode 100644 www/manager6/form/Tag.js
>  create mode 100644 www/manager6/form/TagColorGrid.js
>  create mode 100644 www/manager6/form/TagEdit.js
> 


applied manager parts, thanks! should be complete now, else -> holler at me.




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

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

Thread overview: 25+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-11-16 15:47 [pve-devel] [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Dominik Csapak
2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 1/5] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 2/5] Cluster: add get_guest_config_properties Dominik Csapak
2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 3/5] datacenter.cfg: add option for tag-style Dominik Csapak
2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 4/5] datacenter.cfg: add tag rights control to the datacenter config Dominik Csapak
2022-11-16 15:47 ` [pve-devel] [PATCH cluster v11 5/5] datacenter.cfg: add 'ordering' to 'tag-style' config Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH guest-common v11 1/1] GuestHelpers: add tag related helpers Dominik Csapak
2022-11-17 12:12   ` [pve-devel] applied: " Thomas Lamprecht
2022-11-16 15:48 ` [pve-devel] [PATCH qemu-server v11 1/1] api: update: check for tags permissions with 'assert_tag_permissions' Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH container v11 1/1] check_ct_modify_config_perm: " Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 01/13] api: /cluster/resources: add tags to returned properties Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 02/13] api: allow all users to (partially) read datacenter.cfg Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 03/13] ui: save ui options from /cluster/options instead of version Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 04/13] ui: parse and save tag infos from /cluster/options Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 05/13] ui: add form/TagColorGrid Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 06/13] ui: add PVE.form.ListField Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 07/13] ui: dc/OptionView: add editors for tag settings Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 08/13] ui: add form/Tag Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 09/13] ui: add form/TagEdit.js Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 10/13] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 11/13] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 12/13] ui: add tags to ResourceGrid and GlobalSearchField Dominik Csapak
2022-11-16 15:48 ` [pve-devel] [PATCH manager v11 13/13] ui: implement tag ordering from datacenter.cfg Dominik Csapak
2022-11-17 10:18 ` [pve-devel] partially-applied: [PATCH cluster/guest-common/qemu-server/ct/manager v11] add tags to ui Thomas Lamprecht
2022-11-17 17:22 ` [pve-devel] applied: " Thomas Lamprecht

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