* [pve-devel] [PATCH cluster v6 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-27 7:17 ` Thomas Lamprecht
2022-04-12 13:34 ` [pve-devel] [PATCH cluster v6 2/3] Cluster: add get_guest_config_properties Dominik Csapak
` (13 subsequent siblings)
14 siblings, 1 reply; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
for getting multiple properties from the in memory config of the
guests. I added a new CSF_IPC_ call to maintain backwards compatibility.
It basically behaves the same as
CFS_IPC_GET_GUEST_CONFIG_PROPERTY, but takes a list of properties
instead.
The old way of getting a single property is now also done by
the new function.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
data/src/cfs-ipc-ops.h | 2 +
data/src/server.c | 64 +++++++++++++++
data/src/status.c | 174 ++++++++++++++++++++++++++++-------------
data/src/status.h | 3 +
4 files changed, 187 insertions(+), 56 deletions(-)
diff --git a/data/src/cfs-ipc-ops.h b/data/src/cfs-ipc-ops.h
index 003e233..249308d 100644
--- a/data/src/cfs-ipc-ops.h
+++ b/data/src/cfs-ipc-ops.h
@@ -43,4 +43,6 @@
#define CFS_IPC_VERIFY_TOKEN 12
+#define CFS_IPC_GET_GUEST_CONFIG_PROPERTIES 13
+
#endif
diff --git a/data/src/server.c b/data/src/server.c
index 549788a..c42ff69 100644
--- a/data/src/server.c
+++ b/data/src/server.c
@@ -89,6 +89,13 @@ typedef struct {
char property[];
} cfs_guest_config_propery_get_request_header_t;
+typedef struct {
+ struct qb_ipc_request_header req_header;
+ uint32_t vmid;
+ uint8_t num_props;
+ char props[]; /* list of \0 terminated properties */
+} cfs_guest_config_properties_get_request_header_t;
+
typedef struct {
struct qb_ipc_request_header req_header;
char token[];
@@ -348,6 +355,63 @@ static int32_t s1_msg_process_fn(
result = cfs_create_guest_conf_property_msg(outbuf, memdb, rh->property, rh->vmid);
}
+ } else if (request_id == CFS_IPC_GET_GUEST_CONFIG_PROPERTIES) {
+
+ cfs_guest_config_properties_get_request_header_t *rh =
+ (cfs_guest_config_properties_get_request_header_t *) data;
+
+ size_t remaining = request_size - G_STRUCT_OFFSET(cfs_guest_config_properties_get_request_header_t, props);
+
+ result = 0;
+ if (rh->vmid < 100 && rh->vmid != 0) {
+ cfs_debug("vmid out of range %u", rh->vmid);
+ result = -EINVAL;
+ } else if (rh->vmid >= 100 && !vmlist_vm_exists(rh->vmid)) {
+ result = -ENOENT;
+ } else if (rh->num_props == 0) {
+ cfs_debug("num_props == 0");
+ result = -EINVAL;
+ } else if (remaining <= 1) {
+ cfs_debug("property length <= 1, %ld", remaining);
+ result = -EINVAL;
+ } else {
+ const char **properties = malloc(sizeof(char*) * rh->num_props);
+ char *current = (rh->props);
+ for (uint8_t i = 0; i < rh->num_props; i++) {
+ size_t proplen = strnlen(current, remaining);
+ if (proplen == 0) {
+ cfs_debug("property length 0");
+ result = -EINVAL;
+ break;
+ }
+ if (proplen == remaining) {
+ cfs_debug("property not \\0 terminated");
+ result = -EINVAL;
+ break;
+ }
+ if (current[0] < 'a' || current[0] > 'z') {
+ cfs_debug("property does not start with [a-z]");
+ result = -EINVAL;
+ break;
+ }
+ properties[i] = current;
+ current[proplen] = '\0'; // ensure property is 0 terminated
+ remaining -= (proplen + 1);
+ current += proplen + 1;
+ }
+
+ if (remaining != 0) {
+ cfs_debug("leftover data after parsing %lu properties", rh->num_props);
+ result = -EINVAL;
+ }
+
+ if (result == 0) {
+ cfs_debug("cfs_get_guest_config_properties: basic valid checked, do request");
+ result = cfs_create_guest_conf_properties_msg(outbuf, memdb, properties, rh->num_props, rh->vmid);
+ }
+
+ free(properties);
+ }
} else if (request_id == CFS_IPC_VERIFY_TOKEN) {
cfs_verify_token_request_header_t *rh = (cfs_verify_token_request_header_t *) data;
diff --git a/data/src/status.c b/data/src/status.c
index 9bceaeb..03454ec 100644
--- a/data/src/status.c
+++ b/data/src/status.c
@@ -804,25 +804,52 @@ cfs_create_vmlist_msg(GString *str)
return 0;
}
-// checks the conf for a line starting with '$prop:' and returns the value
-// afterwards, whitout initial whitespace(s), we only deal with the format
-// restricion imposed by our perl VM config parser, main reference is
+// checks if a config line starts with the given prop. if yes, writes a '\0'
+// at the end of the value, and returns the pointer where the value starts
+char *
+_get_property_value_from_line(char *line, int line_len, const char *prop, int prop_len)
+{
+ if (line_len <= prop_len + 1) return NULL;
+
+ if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
+ char *v_start = &line[prop_len + 1];
+
+ // drop initial value whitespaces here already
+ while (*v_start && isspace(*v_start)) v_start++;
+
+ if (!*v_start) return NULL;
+
+ char *v_end = &line[line_len - 1];
+ while (v_end > v_start && isspace(*v_end)) v_end--;
+ v_end[1] = '\0';
+
+ return v_start;
+ }
+
+ return NULL;
+}
+
+// checks the conf for lines starting with the given props and
+// writes the pointers into the correct positions into the 'found' array
+// afterwards, without initial whitespace(s), we only deal with the format
+// restriction imposed by our perl VM config parser, main reference is
// PVE::QemuServer::parse_vm_config this allows to be very fast and still
// relatively simple
-// main restrictions used for our advantage is the properties match reges:
+// main restrictions used for our advantage is the properties match regex:
// ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) from parse_vm_config
// currently we only look at the current configuration in place, i.e., *no*
-// snapshort and *no* pending changes
-static char *
-_get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
+// snapshot and *no* pending changes
+void
+_get_property_values(char **found, char *conf, int conf_size, const char **props, uint8_t num_props, char min, char max)
{
const char *const conf_end = conf + conf_size;
char *line = conf;
- size_t remaining_size;
+ size_t remaining_size = conf_size;
+ int count = 0;
char *next_newline = memchr(conf, '\n', conf_size);
if (next_newline == NULL) {
- return NULL; // valid property lines end with \n, but none in the config
+ return; // valid property lines end with \n, but none in the config
}
*next_newline = '\0';
@@ -830,41 +857,33 @@ _get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
if (!line[0]) goto next;
// snapshot or pending section start, but nothing found yet -> not found
- if (line[0] == '[') return NULL;
- // properties start with /^[a-z]/, so continue early if not
- if (line[0] < 'a' || line[0] > 'z') goto next;
+ if (line[0] == '[') return;
+ // continue early if line does not begin with the min/max char of the properties
+ if (line[0] < min || line[0] > max) goto next;
int line_len = strlen(line);
- if (line_len <= prop_len + 1) goto next;
-
- if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
- char *v_start = &line[prop_len + 1];
-
- // drop initial value whitespaces here already
- while (*v_start && isspace(*v_start)) v_start++;
-
- if (!*v_start) return NULL;
-
- char *v_end = &line[line_len - 1];
- while (v_end > v_start && isspace(*v_end)) v_end--;
- v_end[1] = '\0';
-
- return v_start;
+ for (uint8_t i = 0; i < num_props; i++) {
+ char * value = _get_property_value_from_line(line, line_len, props[i], strlen(props[i]));
+ if (value != NULL) {
+ count += (found[i] != NULL) & 0x1; // count newly found lines
+ found[i] = value;
+ }
+ }
+ if (count == num_props) {
+ // found all
+ return;
}
next:
line = next_newline + 1;
remaining_size = conf_end - line;
- if (remaining_size <= prop_len) {
- return NULL;
- }
next_newline = memchr(line, '\n', remaining_size);
if (next_newline == NULL) {
- return NULL; // valid property lines end with \n, but none in the config
+ return;
}
*next_newline = '\0';
}
- return NULL; // not found
+ return;
}
static void
@@ -883,24 +902,73 @@ _g_str_append_kv_jsonescaped(GString *str, const char *k, const char *v)
}
int
-cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
+_print_found_properties(
+ GString *str,
+ gpointer conf,
+ int size,
+ const char **props,
+ uint8_t num_props,
+ uint32_t vmid,
+ char **values,
+ char min,
+ char max,
+ int first)
+{
+ _get_property_values(values, conf, size, props, num_props, min, max);
+
+ uint8_t found = 0;
+ for (uint8_t i = 0; i < num_props; i++) {
+ if (values[i] != NULL) {
+ if (found) {
+ g_string_append_c(str, ',');
+ } else {
+ if (!first) g_string_append_printf(str, ",\n");
+ else first = 0;
+ g_string_append_printf(str, "\"%u\":{", vmid);
+ found = 1;
+ }
+ _g_str_append_kv_jsonescaped(str, props[i], values[i]);
+ }
+ }
+
+ if (found) {
+ g_string_append_c(str, '}');
+ }
+
+ return first;
+}
+
+int
+cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid)
{
g_return_val_if_fail(cfs_status.vmlist != NULL, -EINVAL);
g_return_val_if_fail(str != NULL, -EINVAL);
- int prop_len = strlen(prop);
- int res = 0;
- GString *path = NULL;
-
// Prelock &memdb->mutex in order to enable the usage of memdb_read_nolock
// to prevent Deadlocks as in #2553
g_mutex_lock (&memdb->mutex);
g_mutex_lock (&mutex);
- g_string_printf(str,"{\n");
+ g_string_printf(str, "{\n");
GHashTable *ht = cfs_status.vmlist;
+
+ int res = 0;
+ GString *path = NULL;
gpointer tmp = NULL;
+ char **values = calloc(num_props, sizeof(char*));
+ char min = 'z', max = 'a';
+
+ for (uint8_t i = 0; i < num_props; i++) {
+ if (props[i][0] > max) {
+ max = props[i][0];
+ }
+
+ if (props[i][0] < min) {
+ min = props[i][0];
+ }
+ }
+
if (!g_hash_table_size(ht)) {
goto ret;
}
@@ -919,15 +987,8 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
// use memdb_read_nolock because lock is handled here
int size = memdb_read_nolock(memdb, path->str, &tmp);
if (tmp == NULL) goto err;
- if (size <= prop_len) goto ret;
-
- char *val = _get_property_value(tmp, size, prop, prop_len);
- if (val == NULL) goto ret;
-
- g_string_append_printf(str, "\"%u\":{", vmid);
- _g_str_append_kv_jsonescaped(str, prop, val);
- g_string_append_c(str, '}');
+ _print_found_properties(str, tmp, size, props, num_props, vmid, values, min, max, 1);
} else {
GHashTableIter iter;
g_hash_table_iter_init (&iter, ht);
@@ -943,21 +1004,16 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
tmp = NULL;
// use memdb_read_nolock because lock is handled here
int size = memdb_read_nolock(memdb, path->str, &tmp);
- if (tmp == NULL || size <= prop_len) continue;
+ if (tmp == NULL) continue;
- char *val = _get_property_value(tmp, size, prop, prop_len);
- if (val == NULL) continue;
-
- if (!first) g_string_append_printf(str, ",\n");
- else first = 0;
-
- g_string_append_printf(str, "\"%u\":{", vminfo->vmid);
- _g_str_append_kv_jsonescaped(str, prop, val);
- g_string_append_c(str, '}');
+ memset(values, 0, sizeof(char*)*num_props); // reset array
+ first = _print_found_properties(str, tmp, size, props, num_props,
+ vminfo->vmid, values, min, max, first);
}
}
ret:
g_free(tmp);
+ free(values);
if (path != NULL) {
g_string_free(path, TRUE);
}
@@ -973,6 +1029,12 @@ enoent:
goto ret;
}
+int
+cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
+{
+ return cfs_create_guest_conf_properties_msg(str, memdb, &prop, 1, vmid);
+}
+
void
record_memdb_change(const char *path)
{
diff --git a/data/src/status.h b/data/src/status.h
index bbf0948..041cb34 100644
--- a/data/src/status.h
+++ b/data/src/status.h
@@ -163,4 +163,7 @@ cfs_create_memberlist_msg(
int
cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid);
+int
+cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid);
+
#endif /* _PVE_STATUS_H_ */
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [pve-devel] [PATCH cluster v6 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method
2022-04-12 13:34 ` [pve-devel] [PATCH cluster v6 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-04-27 7:17 ` Thomas Lamprecht
0 siblings, 0 replies; 18+ messages in thread
From: Thomas Lamprecht @ 2022-04-27 7:17 UTC (permalink / raw)
To: Proxmox VE development discussion, Dominik Csapak
On 12.04.22 15:34, Dominik Csapak wrote:
> for getting multiple properties from the in memory config of the
> guests. I added a new CSF_IPC_ call to maintain backwards compatibility.
"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.
I'd guess that return value is also different?
>
> The old way of getting a single property is now also done by
> the new function.
maybe we could add a bit more info about changes with reasoning and even a benchmark
in the spirit of commit cf1b19d9adc57571b503737188d30a87e6044792 adding the original
fn, comparing numbers with using just one porperty on this vs. the previous fn to
argue that the change impact is (hopefully) negligible.
>
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> data/src/cfs-ipc-ops.h | 2 +
> data/src/server.c | 64 +++++++++++++++
> data/src/status.c | 174 ++++++++++++++++++++++++++++-------------
> data/src/status.h | 3 +
> 4 files changed, 187 insertions(+), 56 deletions(-)
>
> diff --git a/data/src/cfs-ipc-ops.h b/data/src/cfs-ipc-ops.h
> index 003e233..249308d 100644
> --- a/data/src/cfs-ipc-ops.h
> +++ b/data/src/cfs-ipc-ops.h
> @@ -43,4 +43,6 @@
>
> #define CFS_IPC_VERIFY_TOKEN 12
>
> +#define CFS_IPC_GET_GUEST_CONFIG_PROPERTIES 13
> +
> #endif
> diff --git a/data/src/server.c b/data/src/server.c
> index 549788a..c42ff69 100644
> --- a/data/src/server.c
> +++ b/data/src/server.c
> @@ -89,6 +89,13 @@ typedef struct {
> char property[];
> } cfs_guest_config_propery_get_request_header_t;
>
> +typedef struct {
> + struct qb_ipc_request_header req_header;
> + uint32_t vmid;
> + uint8_t num_props;
> + char props[]; /* list of \0 terminated properties */
> +} cfs_guest_config_properties_get_request_header_t;
> +
> typedef struct {
> struct qb_ipc_request_header req_header;
> char token[];
> @@ -348,6 +355,63 @@ static int32_t s1_msg_process_fn(
>
> result = cfs_create_guest_conf_property_msg(outbuf, memdb, rh->property, rh->vmid);
> }
> + } else if (request_id == CFS_IPC_GET_GUEST_CONFIG_PROPERTIES) {
> +
> + cfs_guest_config_properties_get_request_header_t *rh =
> + (cfs_guest_config_properties_get_request_header_t *) data;
> +
> + size_t remaining = request_size - G_STRUCT_OFFSET(cfs_guest_config_properties_get_request_header_t, props);
> +
> + result = 0;
> + if (rh->vmid < 100 && rh->vmid != 0) {
> + cfs_debug("vmid out of range %u", rh->vmid);
> + result = -EINVAL;
> + } else if (rh->vmid >= 100 && !vmlist_vm_exists(rh->vmid)) {
> + result = -ENOENT;
> + } else if (rh->num_props == 0) {
> + cfs_debug("num_props == 0");
> + result = -EINVAL;
> + } else if (remaining <= 1) {
> + cfs_debug("property length <= 1, %ld", remaining);
> + result = -EINVAL;
> + } else {
> + const char **properties = malloc(sizeof(char*) * rh->num_props);
> + char *current = (rh->props);
> + for (uint8_t i = 0; i < rh->num_props; i++) {
> + size_t proplen = strnlen(current, remaining);
> + if (proplen == 0) {
> + cfs_debug("property length 0");
> + result = -EINVAL;
> + break;
> + }
> + if (proplen == remaining) {
> + cfs_debug("property not \\0 terminated");
> + result = -EINVAL;
> + break;
> + }
> + if (current[0] < 'a' || current[0] > 'z') {
> + cfs_debug("property does not start with [a-z]");
> + result = -EINVAL;
> + break;
> + }
> + properties[i] = current;
> + current[proplen] = '\0'; // ensure property is 0 terminated
> + remaining -= (proplen + 1);
> + current += proplen + 1;
> + }
> +
> + if (remaining != 0) {
> + cfs_debug("leftover data after parsing %lu properties", rh->num_props);
> + result = -EINVAL;
> + }
> +
> + if (result == 0) {
> + cfs_debug("cfs_get_guest_config_properties: basic valid checked, do request");
> + result = cfs_create_guest_conf_properties_msg(outbuf, memdb, properties, rh->num_props, rh->vmid);
> + }
> +
> + free(properties);
> + }
> } else if (request_id == CFS_IPC_VERIFY_TOKEN) {
>
> cfs_verify_token_request_header_t *rh = (cfs_verify_token_request_header_t *) data;
> diff --git a/data/src/status.c b/data/src/status.c
> index 9bceaeb..03454ec 100644
> --- a/data/src/status.c
> +++ b/data/src/status.c
> @@ -804,25 +804,52 @@ cfs_create_vmlist_msg(GString *str)
> return 0;
> }
>
> -// checks the conf for a line starting with '$prop:' and returns the value
> -// afterwards, whitout initial whitespace(s), we only deal with the format
> -// restricion imposed by our perl VM config parser, main reference is
> +// checks if a config line starts with the given prop. if yes, writes a '\0'
> +// at the end of the value, and returns the pointer where the value starts
> +char *
> +_get_property_value_from_line(char *line, int line_len, const char *prop, int prop_len)
> +{
> + if (line_len <= prop_len + 1) return NULL;
> +
> + if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
> + char *v_start = &line[prop_len + 1];
> +
> + // drop initial value whitespaces here already
> + while (*v_start && isspace(*v_start)) v_start++;
> +
> + if (!*v_start) return NULL;
> +
> + char *v_end = &line[line_len - 1];
> + while (v_end > v_start && isspace(*v_end)) v_end--;
> + v_end[1] = '\0';
> +
> + return v_start;
> + }
> +
> + return NULL;
> +}
> +
> +// checks the conf for lines starting with the given props and
> +// writes the pointers into the correct positions into the 'found' array
> +// afterwards, without initial whitespace(s), we only deal with the format
> +// restriction imposed by our perl VM config parser, main reference is
> // PVE::QemuServer::parse_vm_config this allows to be very fast and still
> // relatively simple
> -// main restrictions used for our advantage is the properties match reges:
> +// main restrictions used for our advantage is the properties match regex:
> // ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) from parse_vm_config
> // currently we only look at the current configuration in place, i.e., *no*
> -// snapshort and *no* pending changes
> -static char *
> -_get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
> +// snapshot and *no* pending changes
> +void
> +_get_property_values(char **found, char *conf, int conf_size, const char **props, uint8_t num_props, char min, char max)
> {
> const char *const conf_end = conf + conf_size;
> char *line = conf;
> - size_t remaining_size;
> + size_t remaining_size = conf_size;
> + int count = 0;
>
> char *next_newline = memchr(conf, '\n', conf_size);
> if (next_newline == NULL) {
> - return NULL; // valid property lines end with \n, but none in the config
> + return; // valid property lines end with \n, but none in the config
> }
> *next_newline = '\0';
>
> @@ -830,41 +857,33 @@ _get_property_value(char *conf, int conf_size, const char *prop, int prop_len)
> if (!line[0]) goto next;
>
> // snapshot or pending section start, but nothing found yet -> not found
> - if (line[0] == '[') return NULL;
> - // properties start with /^[a-z]/, so continue early if not
> - if (line[0] < 'a' || line[0] > 'z') goto next;
> + if (line[0] == '[') return;
> + // continue early if line does not begin with the min/max char of the properties
> + if (line[0] < min || line[0] > max) goto next;
>
> int line_len = strlen(line);
> - if (line_len <= prop_len + 1) goto next;
> -
> - if (line[prop_len] == ':' && memcmp(line, prop, prop_len) == 0) { // found
> - char *v_start = &line[prop_len + 1];
> -
> - // drop initial value whitespaces here already
> - while (*v_start && isspace(*v_start)) v_start++;
> -
> - if (!*v_start) return NULL;
> -
> - char *v_end = &line[line_len - 1];
> - while (v_end > v_start && isspace(*v_end)) v_end--;
> - v_end[1] = '\0';
> -
> - return v_start;
> + for (uint8_t i = 0; i < num_props; i++) {
> + char * value = _get_property_value_from_line(line, line_len, props[i], strlen(props[i]));
> + if (value != NULL) {
> + count += (found[i] != NULL) & 0x1; // count newly found lines
> + found[i] = value;
> + }
> + }
> + if (count == num_props) {
> + // found all
comment belongs after return, avoid code bloat.
> + return;
> }
> next:
> line = next_newline + 1;
> remaining_size = conf_end - line;
> - if (remaining_size <= prop_len) {
> - return NULL;
> - }
> next_newline = memchr(line, '\n', remaining_size);
> if (next_newline == NULL) {
> - return NULL; // valid property lines end with \n, but none in the config
> + return;
why do you delete the comment?
> }
> *next_newline = '\0';
> }
>
> - return NULL; // not found
> + return;
> }
>
> static void
> @@ -883,24 +902,73 @@ _g_str_append_kv_jsonescaped(GString *str, const char *k, const char *v)
> }
>
> int
> -cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
> +_print_found_properties(
> + GString *str,
> + gpointer conf,
> + int size,
> + const char **props,
> + uint8_t num_props,
> + uint32_t vmid,
> + char **values,
> + char min,
> + char max,
> + int first)
> +{
> + _get_property_values(values, conf, size, props, num_props, min, max);
> +
> + uint8_t found = 0;
> + for (uint8_t i = 0; i < num_props; i++) {
> + if (values[i] != NULL) {
if if (values[i] != NULL) {
continue;
}
> + if (found) {
> + g_string_append_c(str, ',');
> + } else {
> + if (!first) g_string_append_printf(str, ",\n");
> + else first = 0;
I'm somewaht OK with a standalone, single-line if's being without curly brackets and on one line in C,
but please avoid that for more complex ones, i.e., with more than one branch.
> + g_string_append_printf(str, "\"%u\":{", vmid);
> + found = 1;
> + }
> + _g_str_append_kv_jsonescaped(str, props[i], values[i]);
> + }
> + }
> +
> + if (found) {
> + g_string_append_c(str, '}');
> + }
> +
> + return first;
> +}
> +
> +int
> +cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid)
> {
> g_return_val_if_fail(cfs_status.vmlist != NULL, -EINVAL);
> g_return_val_if_fail(str != NULL, -EINVAL);
>
> - int prop_len = strlen(prop);
> - int res = 0;
> - GString *path = NULL;
> -
> // Prelock &memdb->mutex in order to enable the usage of memdb_read_nolock
> // to prevent Deadlocks as in #2553
> g_mutex_lock (&memdb->mutex);
> g_mutex_lock (&mutex);
>
> - g_string_printf(str,"{\n");
> + g_string_printf(str, "{\n");
>
> GHashTable *ht = cfs_status.vmlist;
> +
> + int res = 0;
> + GString *path = NULL;
> gpointer tmp = NULL;
> + char **values = calloc(num_props, sizeof(char*));
> + char min = 'z', max = 'a';
> +
> + for (uint8_t i = 0; i < num_props; i++) {
> + if (props[i][0] > max) {
> + max = props[i][0];
> + }
> +
> + if (props[i][0] < min) {
> + min = props[i][0];
> + }
> + }
> +
> if (!g_hash_table_size(ht)) {
> goto ret;
> }
> @@ -919,15 +987,8 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
> // use memdb_read_nolock because lock is handled here
> int size = memdb_read_nolock(memdb, path->str, &tmp);
> if (tmp == NULL) goto err;
> - if (size <= prop_len) goto ret;
> -
> - char *val = _get_property_value(tmp, size, prop, prop_len);
> - if (val == NULL) goto ret;
> -
> - g_string_append_printf(str, "\"%u\":{", vmid);
> - _g_str_append_kv_jsonescaped(str, prop, val);
> - g_string_append_c(str, '}');
>
> + _print_found_properties(str, tmp, size, props, num_props, vmid, values, min, max, 1);
> } else {
> GHashTableIter iter;
> g_hash_table_iter_init (&iter, ht);
> @@ -943,21 +1004,16 @@ cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *pro
> tmp = NULL;
> // use memdb_read_nolock because lock is handled here
> int size = memdb_read_nolock(memdb, path->str, &tmp);
> - if (tmp == NULL || size <= prop_len) continue;
> + if (tmp == NULL) continue;
>
> - char *val = _get_property_value(tmp, size, prop, prop_len);
> - if (val == NULL) continue;
> -
> - if (!first) g_string_append_printf(str, ",\n");
> - else first = 0;
> -
> - g_string_append_printf(str, "\"%u\":{", vminfo->vmid);
> - _g_str_append_kv_jsonescaped(str, prop, val);
> - g_string_append_c(str, '}');
> + memset(values, 0, sizeof(char*)*num_props); // reset array
> + first = _print_found_properties(str, tmp, size, props, num_props,
> + vminfo->vmid, values, min, max, first);
> }
> }
> ret:
> g_free(tmp);
> + free(values);
> if (path != NULL) {
> g_string_free(path, TRUE);
> }
> @@ -973,6 +1029,12 @@ enoent:
> goto ret;
> }
>
> +int
> +cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid)
> +{
> + return cfs_create_guest_conf_properties_msg(str, memdb, &prop, 1, vmid);
> +}
> +
> void
> record_memdb_change(const char *path)
> {
> diff --git a/data/src/status.h b/data/src/status.h
> index bbf0948..041cb34 100644
> --- a/data/src/status.h
> +++ b/data/src/status.h
> @@ -163,4 +163,7 @@ cfs_create_memberlist_msg(
> int
> cfs_create_guest_conf_property_msg(GString *str, memdb_t *memdb, const char *prop, uint32_t vmid);
>
> +int
> +cfs_create_guest_conf_properties_msg(GString *str, memdb_t *memdb, const char **props, uint8_t num_props, uint32_t vmid);
> +
> #endif /* _PVE_STATUS_H_ */
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH cluster v6 2/3] Cluster: add get_guest_config_properties
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH cluster v6 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH cluster v6 3/3] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
` (12 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
akin to get_guest_config_property, but with a list of properties.
uses the new CFS_IPC_GET_GUEST_CONFIG_PROPERTIES
also adds the same NOTEs regarding parsing/permissions to the comment
of get_guest_config_property
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
data/PVE/Cluster.pm | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/data/PVE/Cluster.pm b/data/PVE/Cluster.pm
index 05451fd..d0148fc 100644
--- a/data/PVE/Cluster.pm
+++ b/data/PVE/Cluster.pm
@@ -340,10 +340,37 @@ sub get_node_kv {
return $res;
}
+# properties: an array-ref of config properties you want to get, e.g., this
+# is perfect to get multiple properties of a guest _fast_
+# (>100 faster than manual parsing here)
+# vmid: optional, if a valid is passed we only check that one, else return all
+# NOTE: does *not* searches snapshot and PENDING entries sections!
+# NOTE: returns the guest config lines (excluding trailing whitespace) as is,
+# so for non-trivial properties, checking the validity must be done
+# NOTE: no permission check is done, that is the responsibilty of the caller
+sub get_guest_config_properties {
+ my ($properties, $vmid) = @_;
+
+ die "properties required" if !defined($properties);
+
+ my $num_props = scalar(@$properties);
+ die "only up to 255 properties supported" if $num_props > 255;
+ my $bindata = pack "VC", $vmid // 0, $num_props;
+ for my $property (@$properties) {
+ $bindata .= pack "Z*", $property;
+ }
+ my $res = $ipcc_send_rec_json->(CFS_IPC_GET_GUEST_CONFIG_PROPERTIES, $bindata);
+
+ return $res;
+}
+
# property: a config property you want to get, e.g., this is perfect to get
# the 'lock' entry of a guest _fast_ (>100 faster than manual parsing here)
# vmid: optional, if a valid is passed we only check that one, else return all
# NOTE: does *not* searches snapshot and PENDING entries sections!
+# NOTE: returns the guest config lines (excluding trailing whitespace) as is,
+# so for non-trivial properties, checking the validity must be done
+# NOTE: no permission check is done, that is the responsibilty of the caller
sub get_guest_config_property {
my ($property, $vmid) = @_;
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH cluster v6 3/3] datacenter.cfg: add option for tag-tree-style and tag-colors
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH cluster v6 1/3] add CFS_IPC_GET_GUEST_CONFIG_PROPERTIES method Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH cluster v6 2/3] Cluster: add get_guest_config_properties Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-27 5:55 ` Thomas Lamprecht
2022-04-12 13:34 ` [pve-devel] [PATCH widget-toolkit v6 1/1] add tag related helpers Dominik Csapak
` (11 subsequent siblings)
14 siblings, 1 reply; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
data/PVE/DataCenterConfig.pm | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
index 6c0fa5b..c21dbb7 100644
--- a/data/PVE/DataCenterConfig.pm
+++ b/data/PVE/DataCenterConfig.pm
@@ -106,6 +106,10 @@ sub pve_verify_mac_prefix {
return $mac_prefix;
}
+my $TAG_RE = '[a-zA-Z0-9_][a-zA-Z0-9_\-\+\.]*';
+my $COLOR_RE = '[0-9a-fA-F]{6}';
+my $OVERRIDE_RE = "(?:${TAG_RE}=${COLOR_RE}(?:\:${COLOR_RE})?)";
+
my $datacenter_schema = {
type => "object",
additionalProperties => 0,
@@ -222,6 +226,20 @@ my $datacenter_schema = {
maxLength => 64 * 1024,
optional => 1,
},
+ 'tag-tree-style' => {
+ optional => 1,
+ type => 'string',
+ enum => ['full', 'circle', 'dense', 'none'],
+ default => 'circle',
+ description => "Tag style in tree.",
+ },
+ 'tag-colors' => {
+ optional => 1,
+ type => 'string',
+ pattern => "${OVERRIDE_RE}(?:\,$OVERRIDE_RE)*",
+ typetext => '<tag>=<hex-color>[:<hex-color-for-text>][,<tag>=...]',
+ description => "Manual color mapping for tags (comma separated).",
+ },
},
};
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [pve-devel] [PATCH cluster v6 3/3] datacenter.cfg: add option for tag-tree-style and tag-colors
2022-04-12 13:34 ` [pve-devel] [PATCH cluster v6 3/3] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
@ 2022-04-27 5:55 ` Thomas Lamprecht
0 siblings, 0 replies; 18+ messages in thread
From: Thomas Lamprecht @ 2022-04-27 5:55 UTC (permalink / raw)
To: Proxmox VE development discussion, Dominik Csapak
On 12.04.22 15:34, Dominik Csapak wrote:
something ate the commit message ;-)
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
> ---
> data/PVE/DataCenterConfig.pm | 18 ++++++++++++++++++
> 1 file changed, 18 insertions(+)
>
> diff --git a/data/PVE/DataCenterConfig.pm b/data/PVE/DataCenterConfig.pm
> index 6c0fa5b..c21dbb7 100644
> --- a/data/PVE/DataCenterConfig.pm
> +++ b/data/PVE/DataCenterConfig.pm
> @@ -106,6 +106,10 @@ sub pve_verify_mac_prefix {
> return $mac_prefix;
> }
>
> +my $TAG_RE = '[a-zA-Z0-9_][a-zA-Z0-9_\-\+\.]*';
> +my $COLOR_RE = '[0-9a-fA-F]{6}';
> +my $OVERRIDE_RE = "(?:${TAG_RE}=${COLOR_RE}(?:\:${COLOR_RE})?)";
> +
> my $datacenter_schema = {
> type => "object",
> additionalProperties => 0,
> @@ -222,6 +226,20 @@ my $datacenter_schema = {
> maxLength => 64 * 1024,
> optional => 1,
> },
> + 'tag-tree-style' => {
> + optional => 1,
> + type => 'string',
> + enum => ['full', 'circle', 'dense', 'none'],
what's full vs. dense and why is the description not mentioning that?
Also, isn't this less the style but more the shape? (but see below)
> + default => 'circle',
> + description => "Tag style in tree.",
> + },
> + 'tag-colors' => {
why don't we get a `tag-style` format-string property with a format that has
`colors` and `shape` as members?
the color list format would just need to change to: <tag>:<color>[;<tag>:color>]
> + optional => 1,
> + type => 'string',
> + pattern => "${OVERRIDE_RE}(?:\,$OVERRIDE_RE)*",
sorry, but OVERRIDE_RE is really not a good variable name to use for a module wide regex,
why not either $TAG_COLOR_OVERRIDE_RE or just set it inline?
> + typetext => '<tag>=<hex-color>[:<hex-color-for-text>][,<tag>=...]',
> + description => "Manual color mapping for tags (comma separated).",
> + },
> },
> };
>
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH widget-toolkit v6 1/1] add tag related helpers
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (2 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH cluster v6 3/3] datacenter.cfg: add option for tag-tree-style and tag-colors Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 01/11] api: /cluster/resources: add tags to returned properties Dominik Csapak
` (10 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
helpers to
* generate a color from a string consistently
* generate a html tag for a tag
* related css classes
contrast is calculated according to SAPC draft:
https://github.com/Myndex/SAPC-APCA
which is likely to become a w3c guideline in the future and seems
to be a better algorithm for this
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
src/Utils.js | 90 ++++++++++++++++++++++++++++++++++++++++++++
src/css/ext6-pmx.css | 45 ++++++++++++++++++++++
2 files changed, 135 insertions(+)
diff --git a/src/Utils.js b/src/Utils.js
index 6a03057..eb13838 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1272,6 +1272,96 @@ utilities: {
.map(val => val.charCodeAt(0)),
);
},
+
+ stringToRGB: function(string) {
+ let hash = 0;
+ if (!string) {
+ return hash;
+ }
+ string += 'prox'; // give short strings more variance
+ for (let i = 0; i < string.length; i++) {
+ hash = string.charCodeAt(i) + ((hash << 5) - hash);
+ hash = hash & hash; // to int
+ }
+
+ let alpha = 0.7; // make the color a bit brighter
+ let bg = 255; // assume white background
+
+ return [
+ (hash & 255)*alpha + bg*(1-alpha),
+ ((hash >> 8) & 255)*alpha + bg*(1-alpha),
+ ((hash >> 16) & 255)*alpha + bg*(1-alpha),
+ ];
+ },
+
+ rgbToCss: function(rgb) {
+ return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
+ },
+
+ rgbToHex: function(rgb) {
+ let r = Math.round(rgb[0]).toString(16);
+ let g = Math.round(rgb[1]).toString(16);
+ let b = Math.round(rgb[2]).toString(16);
+ return `${r}${g}${b}`;
+ },
+
+ hexToRGB: function(hex) {
+ if (!hex) {
+ return undefined;
+ }
+ if (hex.length === 7) {
+ hex = hex.slice(1);
+ }
+ let r = parseInt(hex.slice(0, 2), 16);
+ let g = parseInt(hex.slice(2, 4), 16);
+ let b = parseInt(hex.slice(4, 6), 16);
+ return [r, g, b];
+ },
+
+ // optimized & simplified SAPC function
+ // https://github.com/Myndex/SAPC-APCA
+ getTextContrastClass: function(rgb) {
+ const blkThrs = 0.022;
+ const blkClmp = 1.414;
+
+ // linearize & gamma correction
+ let r = (rgb[0]/255)**2.4;
+ let g = (rgb[1]/255)**2.4;
+ let b = (rgb[2]/255)**2.4;
+
+ // relative luminance sRGB
+ let bg = r*0.2126729 + g*0.7151522 + b*0.0721750;
+
+ // black clamp
+ bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp;
+
+ // SAPC with white text
+ let contrastLight = bg ** 0.65 - 1;
+ // SAPC with black text
+ let contrastDark = bg ** 0.56 - 0.046134502;
+
+ if (Math.abs(contrastLight) >= Math.abs(contrastDark)) {
+ return 'light';
+ } else {
+ return 'dark';
+ }
+ },
+
+ getTagElement: function(string, color_overrides) {
+ let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string);
+ let bgcolor = Proxmox.Utils.rgbToCss(rgb);
+ let style = `background-color: ${bgcolor};`;
+ let cls;
+ if (rgb.length > 3) {
+ let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]);
+ style += `color: ${fgcolor}`;
+ cls = "proxmox-tag-dark";
+ } else {
+ let txtCls = Proxmox.Utils.getTextContrastClass(rgb);
+ cls = `proxmox-tag-${txtCls}`;
+ }
+ return `<span class="${cls}" style="${style}">${string}</span>`;
+ },
},
singleton: true,
diff --git a/src/css/ext6-pmx.css b/src/css/ext6-pmx.css
index 2516578..4448751 100644
--- a/src/css/ext6-pmx.css
+++ b/src/css/ext6-pmx.css
@@ -6,6 +6,51 @@
background-color: LightYellow;
}
+.proxmox-tags-full .proxmox-tag-light,
+.proxmox-tags-full .proxmox-tag-dark {
+ border-radius: 3px;
+ padding: 1px 6px;
+ margin: 0px 1px;
+}
+
+.proxmox-tags-circle .proxmox-tag-light,
+.proxmox-tags-circle .proxmox-tag-dark {
+ margin: 0px 1px;
+ position: relative;
+ top: 2px;
+ border-radius: 6px;
+ height: 12px;
+ width: 12px;
+ display: inline-block;
+ color: transparent !important;
+ overflow: hidden;
+}
+
+.proxmox-tags-none .proxmox-tag-light,
+.proxmox-tags-none .proxmox-tag-dark {
+ display: none;
+}
+
+.proxmox-tags-dense .proxmox-tag-light,
+.proxmox-tags-dense .proxmox-tag-dark {
+ width: 6px;
+ margin-right: 1px;
+ display: inline-block;
+ color: transparent !important;
+ overflow: hidden;
+ vertical-align: bottom;
+}
+
+.proxmox-tags-full .proxmox-tag-light {
+ color: #fff;
+ background-color: #383838;
+}
+
+.proxmox-tags-full .proxmox-tag-dark {
+ color: #000;
+ background-color: #f0f0f0;
+}
+
.x-mask-msg-text {
text-align: center;
}
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 01/11] api: /cluster/resources: add tags to returned properties
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (3 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH widget-toolkit v6 1/1] add tag related helpers Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 02/11] api: /version: add 'tag-colors' and 'tag-tree-style' Dominik Csapak
` (9 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
by querying 'lock' and 'tags' with 'get_guest_config_properties'
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
PVE/API2/Cluster.pm | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/PVE/API2/Cluster.pm b/PVE/API2/Cluster.pm
index 718a8eb9..01a21c9e 100644
--- a/PVE/API2/Cluster.pm
+++ b/PVE/API2/Cluster.pm
@@ -360,7 +360,8 @@ __PACKAGE__->register_method({
# we try to generate 'numbers' by using "$X + 0"
if (!$param->{type} || $param->{type} eq 'vm') {
- my $locked_vms = PVE::Cluster::get_guest_config_property('lock');
+ my $prop_list = [qw(lock tags)];
+ my $props = PVE::Cluster::get_guest_config_properties($prop_list);
for my $vmid (sort keys %$idlist) {
@@ -392,8 +393,10 @@ __PACKAGE__->register_method({
# only skip now to next to ensure that the pool stats above are filled, if eligible
next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
- if (defined(my $lock = $locked_vms->{$vmid}->{lock})) {
- $entry->{lock} = $lock;
+ for my $prop (@$prop_list) {
+ if (defined(my $value = $props->{$vmid}->{$prop})) {
+ $entry->{$prop} = $value;
+ }
}
if (defined($entry->{pool}) &&
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 02/11] api: /version: add 'tag-colors' and 'tag-tree-style'
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (4 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 01/11] api: /cluster/resources: add tags to returned properties Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 03/11] ui: parse and save tag color overrides from /version Dominik Csapak
` (8 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
to be able to get them in the gui directly after login
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
PVE/API2.pm | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/PVE/API2.pm b/PVE/API2.pm
index a4256160..0592d4de 100644
--- a/PVE/API2.pm
+++ b/PVE/API2.pm
@@ -111,6 +111,16 @@ __PACKAGE__->register_method ({
optional => 1,
description => 'The default console viewer to use.',
},
+ 'tag-colors' => {
+ type => 'string',
+ optional => 1,
+ description => 'Cluster wide tag color overrides',
+ },
+ 'tag-tree-style' => {
+ type => 'string',
+ optional => 1,
+ description => 'Tag style in tree',
+ },
},
},
code => sub {
@@ -119,7 +129,7 @@ __PACKAGE__->register_method ({
my $res = {};
my $datacenter_confg = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
- for my $k (qw(console)) {
+ for my $k (qw(console tag-colors tag-tree-style)) {
$res->{$k} = $datacenter_confg->{$k} if exists $datacenter_confg->{$k};
}
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 03/11] ui: parse and save tag color overrides from /version
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (5 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 02/11] api: /version: add 'tag-colors' and 'tag-tree-style' Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 04/11] ui: tree/ResourceTree: collect tags on update Dominik Csapak
` (7 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
into a global list of overrides. on update, also parse the values
from the browser localstore
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Utils.js | 39 +++++++++++++++++++++++++++++++++++++++
www/manager6/Workspace.js | 13 +++++++++++++
2 files changed, 52 insertions(+)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index fe5be283..61d93b5d 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1815,6 +1815,45 @@ Ext.define('PVE.Utils', {
return undefined;
},
+
+ parseTagOverrides: function(overrides) {
+ let colors = {};
+ (overrides || "").split(/[;, ]/).forEach(color => {
+ if (!color) {
+ return;
+ }
+ let [tag, color_hex] = color.split('=', 2);
+ let r = parseInt(color_hex.slice(0, 2), 16);
+ let g = parseInt(color_hex.slice(2, 4), 16);
+ let b = parseInt(color_hex.slice(4, 6), 16);
+ colors[tag] = [r, g, b];
+ if (color_hex.length === 13) {
+ colors[tag].push(parseInt(color_hex.slice(7, 9), 16));
+ colors[tag].push(parseInt(color_hex.slice(9, 11), 16));
+ colors[tag].push(parseInt(color_hex.slice(11, 13), 16));
+ }
+ });
+ return colors;
+ },
+
+ tagOverrides: {},
+
+ updateTagOverrides: function(colors) {
+ let sp = Ext.state.Manager.getProvider();
+ let color_state = sp.get('colors', '');
+ let browser_colors = PVE.Utils.parseTagOverrides(color_state);
+ PVE.Utils.tagOverrides = Ext.apply({}, browser_colors, colors);
+ },
+
+ updateTagSettings: function(overrides, style) {
+ PVE.Utils.updateTagOverrides(PVE.Utils.parseTagOverrides(overrides ?? ""));
+
+ if (style === undefined || style === '__default__') {
+ style = 'circle';
+ }
+
+ Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
+ },
},
singleton: true,
diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js
index 37d772b8..d9875c18 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -155,6 +155,7 @@ Ext.define('PVE.StdWorkspace', {
success: function(response) {
PVE.VersionInfo = response.result.data;
me.updateVersionInfo();
+ me.updateTags();
},
});
@@ -213,6 +214,18 @@ Ext.define('PVE.StdWorkspace', {
ui.updateLayout();
},
+ updateTags: function() {
+ let me = this;
+ let colors = PVE.VersionInfo?.['tag-colors'];
+ let style = PVE.VersionInfo?.['tag-tree-style'];
+
+ PVE.Utils.updateTagSettings(colors, style);
+ if (colors) {
+ // refresh tree once
+ PVE.data.ResourceStore.fireEvent('load');
+ }
+ },
+
initComponent: function() {
let me = this;
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 04/11] ui: tree/ResourceTree: collect tags on update
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (6 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 03/11] ui: parse and save tag color overrides from /version Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 05/11] ui: add form/TagColorGrid Dominik Csapak
` (6 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
into a global list, so that we have it avaiable anywhere
also add the tags from the tagOverrides on update into the list
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Utils.js | 7 +++++++
www/manager6/data/ResourceStore.js | 6 ++++++
www/manager6/tree/ResourceTree.js | 16 ++++++++++++++--
3 files changed, 27 insertions(+), 2 deletions(-)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 61d93b5d..ff86f16b 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1816,6 +1816,13 @@ Ext.define('PVE.Utils', {
return undefined;
},
+ tagList: new Set(),
+
+ updateTagList: function(tags) {
+ let override_tags = Object.keys(PVE.Utils.tagOverrides);
+ PVE.Utils.tagList = [...new Set([...tags, ...override_tags])].sort();
+ },
+
parseTagOverrides: function(overrides) {
let colors = {};
(overrides || "").split(/[;, ]/).forEach(color => {
diff --git a/www/manager6/data/ResourceStore.js b/www/manager6/data/ResourceStore.js
index c7b72306..b18f7dd8 100644
--- a/www/manager6/data/ResourceStore.js
+++ b/www/manager6/data/ResourceStore.js
@@ -293,6 +293,12 @@ Ext.define('PVE.data.ResourceStore', {
sortable: true,
width: 100,
},
+ tags: {
+ header: gettext('Tags'),
+ type: 'string',
+ hidden: true,
+ sortable: true,
+ },
};
let fields = [];
diff --git a/www/manager6/tree/ResourceTree.js b/www/manager6/tree/ResourceTree.js
index be90d4f7..139defab 100644
--- a/www/manager6/tree/ResourceTree.js
+++ b/www/manager6/tree/ResourceTree.js
@@ -226,6 +226,10 @@ Ext.define('PVE.tree.ResourceTree', {
let stateid = 'rid';
+ const changedFields = [
+ 'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags',
+ ];
+
let updateTree = function() {
store.suspendEvents();
@@ -261,7 +265,7 @@ Ext.define('PVE.tree.ResourceTree', {
}
// tree item has been updated
- for (const field of ['text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock']) {
+ for (const field of changedFields) {
if (item.data[field] !== olditem.data[field]) {
changed = true;
break;
@@ -294,7 +298,14 @@ Ext.define('PVE.tree.ResourceTree', {
}
}
- rstore.each(function(item) { // add new items
+ let tags = new Set();
+
+ rstore.each(function(item) { // add new items and collect tags
+ if (item.data.tags) {
+ item.data.tags.split(/[,; ]/).filter(t => !!t).forEach((tag) => {
+ tags.add(tag);
+ });
+ }
let olditem = index[item.data.id];
if (olditem) {
return;
@@ -310,6 +321,7 @@ Ext.define('PVE.tree.ResourceTree', {
}
});
+ PVE.Utils.updateTagList(tags);
store.resumeEvents();
store.fireEvent('refresh', store);
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 05/11] ui: add form/TagColorGrid
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (7 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 04/11] ui: tree/ResourceTree: collect tags on update Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 06/11] ui: dc/OptionView: add editors for tag settings Dominik Csapak
` (5 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
this provides a basic grid to edit a list of tag color overrides.
We'll use this for editing the datacenter.cfg overrides and the
browser storage overrides.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/css/ext6-pve.css | 5 +
www/manager6/Makefile | 1 +
www/manager6/Utils.js | 2 +
www/manager6/form/TagColorGrid.js | 355 ++++++++++++++++++++++++++++++
4 files changed, 363 insertions(+)
create mode 100644 www/manager6/form/TagColorGrid.js
diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index dadb84a9..f7d0c420 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -651,3 +651,8 @@ table.osds td:first-of-type {
background-color: rgb(245, 245, 245);
color: #000;
}
+
+.x-pveColorPicker-default-cell > .x-grid-cell-inner {
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index d488c3a8..8d14f684 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 ff86f16b..63687d08 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1861,6 +1861,8 @@ Ext.define('PVE.Utils', {
Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
},
+
+ tagCharRegex: /^[a-z0-9+_.-]$/i,
},
singleton: true,
diff --git a/www/manager6/form/TagColorGrid.js b/www/manager6/form/TagColorGrid.js
new file mode 100644
index 00000000..f2c248b3
--- /dev/null
+++ b/www/manager6/form/TagColorGrid.js
@@ -0,0 +1,355 @@
+Ext.define('PVE.form.ColorPicker', {
+ extend: 'Ext.form.FieldContainer',
+ alias: 'widget.pveColorPicker',
+
+ defaultBindProperty: 'value',
+
+ config: {
+ value: null,
+ },
+
+ height: 24,
+
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+
+ getValue: function() {
+ return this.realvalue.slice(1);
+ },
+
+ setValue: function(value) {
+ let me = this;
+ me.setColor(value);
+ if (value && value.length === 6) {
+ me.picker.value = value[0] !== '#' ? `#${value}` : value;
+ }
+ },
+
+ setColor: function(value) {
+ let me = this;
+ let oldValue = me.realvalue;
+ me.realvalue = value;
+ let color = value.length === 6 ? `#${value}` : undefined;
+ me.down('#picker').setStyle('background-color', color);
+ me.down('#text').setValue(value ?? "");
+ me.fireEvent('change', me, me.realvalue, oldValue);
+ },
+
+ initComponent: function() {
+ let me = this;
+ me.picker = document.createElement('input');
+ me.picker.type = 'color';
+ me.picker.style = `opacity: 0; border: 0px; width: 100%; height: ${me.height}px`;
+ me.picker.value = `${me.value}`;
+
+ me.items = [
+ {
+ xtype: 'textfield',
+ itemId: 'text',
+ minLength: !me.allowBlank ? 6 : undefined,
+ maxLength: 6,
+ enforceMaxLength: true,
+ allowBlank: me.allowBlank,
+ emptyText: me.allowBlank ? gettext('Automatic') : undefined,
+ maskRe: /[a-f0-9]/i,
+ regex: /^[a-f0-9]{6}$/i,
+ flex: 1,
+ listeners: {
+ change: function(field, value) {
+ me.setValue(value);
+ },
+ },
+ },
+ {
+ xtype: 'box',
+ style: {
+ 'margin-left': '1px',
+ border: '1px solid #cfcfcf',
+ },
+ itemId: 'picker',
+ width: 24,
+ contentEl: me.picker,
+ },
+ ];
+
+ me.callParent();
+ me.picker.oninput = function() {
+ me.setColor(me.picker.value.slice(1));
+ };
+ },
+});
+
+Ext.define('PVE.form.TagColorGrid', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveTagColorGrid',
+
+ mixins: [
+ 'Ext.form.field.Field',
+ ],
+
+ allowBlank: true,
+ selectAll: false,
+ isFormField: true,
+ deleteEmpty: false,
+ selModel: 'checkboxmodel',
+
+ config: {
+ deleteEmpty: false,
+ },
+
+ emptyText: gettext('No Overrides'),
+ viewConfig: {
+ deferEmptyText: false,
+ },
+
+ setValue: function(value) {
+ let me = this;
+ if (!value) {
+ me.getStore().removeAll();
+ me.checkChange();
+ return me;
+ }
+ let entries = (value.split(/[;, ]/) || []).map((entry) => {
+ let [tag, color] = entry.split(/[=]/);
+ let bg = color;
+ let fg = "";
+ if (color.length > 6) {
+ [bg, fg] = color.split(/[:]/);
+ }
+ return {
+ tag,
+ color: bg,
+ text: fg,
+ };
+ });
+ me.getStore().setData(entries);
+ me.checkChange();
+ return me;
+ },
+
+ getValue: function() {
+ let me = this;
+ let values = [];
+ me.getStore().each((rec) => {
+ if (rec.data.tag) {
+ let val = `${rec.data.tag}=${rec.data.color}`;
+ if (rec.data.text) {
+ val += `:${rec.data.text}`;
+ }
+ values.push(val);
+ }
+ });
+ return values.join(',');
+ },
+
+ getErrors: function(value) {
+ let me = this;
+ let emptyTag = false;
+ let notValidColor = false;
+ let colorRegex = new RegExp(/^[0-9a-f]{6}$/i);
+ me.getStore().each((rec) => {
+ if (!rec.data.tag) {
+ emptyTag = true;
+ }
+ if (!rec.data.color?.match(colorRegex)) {
+ notValidColor = true;
+ }
+ if (rec.data.text && !rec.data.text?.match(colorRegex)) {
+ notValidColor = true;
+ }
+ });
+ let errors = [];
+ if (emptyTag) {
+ errors.push(gettext('Tag must not be empty.'));
+ }
+ if (notValidColor) {
+ errors.push(gettext('Not a valid color.'));
+ }
+ return errors;
+ },
+
+ // override framework function to implement deleteEmpty behaviour
+ getSubmitData: function() {
+ let me = this,
+ data = null,
+ val;
+ if (!me.disabled && me.submitValue) {
+ val = me.getValue();
+ if (val !== null && val !== '') {
+ data = {};
+ data[me.getName()] = val;
+ } else if (me.getDeleteEmpty()) {
+ data = {};
+ data.delete = me.getName();
+ }
+ }
+ return data;
+ },
+
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ addLine: function() {
+ let me = this;
+ me.getView().getStore().add({
+ tag: '',
+ color: '',
+ text: '',
+ });
+ },
+
+ removeSelection: function() {
+ let me = this;
+ let view = me.getView();
+ let selection = view.getSelection();
+ if (selection === undefined) {
+ return;
+ }
+
+ selection.forEach((sel) => {
+ view.getStore().remove(sel);
+ });
+ view.checkChange();
+ },
+
+ 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] 18+ messages in thread
* [pve-devel] [PATCH manager v6 06/11] ui: dc/OptionView: add editors for tag settings
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (8 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 05/11] ui: add form/TagColorGrid Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 07/11] ui: add form/Tag Dominik Csapak
` (4 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
namely for 'tag-tree-style' and 'tag-colors'.
display the tag overrides directly as they will appear as tags
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Utils.js | 20 ++++++++++++++++
www/manager6/dc/OptionView.js | 43 ++++++++++++++++++++++++++++++++++-
2 files changed, 62 insertions(+), 1 deletion(-)
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 63687d08..2fe823ff 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1862,6 +1862,26 @@ Ext.define('PVE.Utils', {
Ext.ComponentQuery.query('pveResourceTree')[0].setUserCls(`proxmox-tags-${style}`);
},
+ tagTreeStyles: {
+ '__default__': Proxmox.Utils.defaultText,
+ 'full': gettext('Full'),
+ 'circle': gettext('Circle'),
+ 'dense': gettext('Dense'),
+ 'none': Proxmox.Utils.NoneText,
+ },
+
+ renderTags: function(tagstext, overrides) {
+ let text = '';
+ if (tagstext) {
+ let tags = (tagstext.split(/[,; ]/) || []).filter(t => !!t);
+ text += ' ';
+ tags.forEach((tag) => {
+ text += Proxmox.Utils.getTagElement(tag, overrides);
+ });
+ }
+ return text;
+ },
+
tagCharRegex: /^[a-z0-9+_.-]$/i,
},
diff --git a/www/manager6/dc/OptionView.js b/www/manager6/dc/OptionView.js
index 6b30ede9..aef77964 100644
--- a/www/manager6/dc/OptionView.js
+++ b/www/manager6/dc/OptionView.js
@@ -5,6 +5,7 @@ Ext.define('PVE.dc.OptionView', {
onlineHelp: 'datacenter_configuration_file',
monStoreErrors: true,
+ userCls: 'proxmox-tags-full',
add_inputpanel_row: function(name, text, opts) {
var me = this;
@@ -284,7 +285,43 @@ Ext.define('PVE.dc.OptionView', {
minValue: 1,
maxValue: 64, // arbitrary but generous limit as limits are good
});
-
+ me.add_combobox_row('tag-tree-style', gettext('Tag Tree Style'), {
+ renderer: (value) => PVE.Utils.tagTreeStyles[value] ?? value,
+ comboItems: Object.entries(PVE.Utils.tagTreeStyles),
+ defaultValue: '__default__',
+ deleteEmpty: true,
+ });
+ me.rows['tag-colors'] = {
+ required: true,
+ renderer: (value) => {
+ if (value === undefined) {
+ return gettext('No Overrides');
+ }
+ let overrides = PVE.Utils.parseTagOverrides(value);
+ let txt = '';
+ for (const tag of Object.keys(overrides)) {
+ txt += Proxmox.Utils.getTagElement(tag, overrides);
+ }
+ return txt;
+ },
+ header: gettext('Tag Color Override'),
+ editor: {
+ xtype: 'proxmoxWindowEdit',
+ width: 800,
+ bodyPadding: 0,
+ subject: gettext('Tag Color Override'),
+ fieldDefaults: {
+ labelWidth: 100,
+ },
+ url: '/api2/extjs/cluster/options',
+ items: [{
+ name: 'tag-colors',
+ xtype: 'pveTagColorGrid',
+ deleteEmpty: true,
+ height: 300,
+ }],
+ },
+ };
me.selModel = Ext.create('Ext.selection.RowModel', {});
Ext.apply(me, {
@@ -319,6 +356,10 @@ Ext.define('PVE.dc.OptionView', {
if (rec.data.value === '__default__') {
delete PVE.VersionInfo.console;
}
+
+ let colors = store.getById('tag-colors')?.data?.value;
+ let style = store.getById('tag-tree-style')?.data?.value;
+ PVE.Utils.updateTagSettings(colors, style);
});
me.on('activate', me.rstore.startUpdate);
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 07/11] ui: add form/Tag
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (9 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 06/11] ui: dc/OptionView: add editors for tag settings Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 08/11] ui: add form/TagEdit.js Dominik Csapak
` (3 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
displays a single tag, with the ability to edit inline on click (when
the mode is set to editable). This brings up a list of globally available tags
for simple selection.
Also has a mode for adding a new Tag.
This has a 'layoutCallback' which will be called on input, so that the parent
component can update the layout when the content changes.
This is necessary since we circumvent the extjs logic for updating.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/form/Tag.js | 270 +++++++++++++++++++++++++++++++++++++++
2 files changed, 271 insertions(+)
create mode 100644 www/manager6/form/Tag.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 8d14f684..b0729f7a 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -74,6 +74,7 @@ JSSRC= \
form/ViewSelector.js \
form/iScsiProviderSelector.js \
form/TagColorGrid.js \
+ form/Tag.js \
grid/BackupView.js \
grid/FirewallAliases.js \
grid/FirewallOptions.js \
diff --git a/www/manager6/form/Tag.js b/www/manager6/form/Tag.js
new file mode 100644
index 00000000..f6caa412
--- /dev/null
+++ b/www/manager6/form/Tag.js
@@ -0,0 +1,270 @@
+Ext.define('Proxmox.Tag', {
+ extend: 'Ext.Component',
+ alias: 'widget.pmxTag',
+
+ // if set to true, displays 'Add Tag' and a plus symbol
+ addTag: false,
+
+ // callback to update the layout in the containing element
+ // this is necessary since we circumvent extjs layout with 'contentEditable'
+ layoutCallback: Ext.emptyFn,
+
+ style: {
+ 'white-space': 'nowrap',
+ },
+
+ icons: {
+ addTag: 'plus',
+ editable: 'minus',
+ normal: '',
+ edit: 'check',
+ },
+
+ faIconStyle: '-square',
+
+ mode: 'normal',
+
+ // we need to do this in mousedown, because that triggers before
+ // focusleave (which triggers before click)
+ onMouseDown: function(event) {
+ let me = this;
+ if (event.target.tagName !== 'I') {
+ return;
+ }
+ switch (me.mode) {
+ case 'editable':
+ if (me.addTag) {
+ break;
+ }
+ me.setText('');
+ me.finishEdit();
+ break;
+ case 'edit':
+ if (me.addTag && me.tagEl().innerHTML === '') {
+ event.browserEvent.preventDefault();
+ event.browserEvent.stopPropagation();
+ break;
+ }
+ me.finishEdit();
+ break;
+ default: break;
+ }
+ },
+
+ onClick: function(event) {
+ let me = this;
+ if (event.target.tagName !== 'SPAN' && !me.addTag) {
+ return;
+ }
+ if (me.mode === 'editable') {
+ me.startEdit();
+ }
+ },
+
+ startEdit: function() {
+ let me = this;
+ me.setMode('edit');
+
+ // select text in the element
+ let range = document.createRange();
+ range.selectNodeContents(me.tagEl());
+ let sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+
+ me.showPicker();
+ },
+
+ showPicker: function() {
+ let me = this;
+ if (!me.picker) {
+ me.picker = Ext.widget({
+ xtype: 'boundlist',
+ minWidth: 70,
+ scrollable: true,
+ floating: true,
+ hidden: true,
+ userCls: 'proxmox-tags-full',
+ displayField: 'tag',
+ itemTpl: [
+ '{[Proxmox.Utils.getTagElement(values.tag, PVE.Utils.tagOverrides)]}',
+ ],
+ store: [],
+ listeners: {
+ select: function(picker, rec) {
+ me.setText(rec.data.tag);
+ me.finishEdit();
+ },
+ },
+ });
+ }
+ me.picker.getStore().clearFilter();
+ let taglist = PVE.Utils.tagList.map(v => ({ tag: v }));
+ if (taglist.length < 1) {
+ return;
+ }
+ me.picker.getStore().setData(taglist);
+ me.picker.showBy(me, 'tl-bl');
+ me.picker.setMaxHeight(200);
+ },
+
+ finishEdit: function(update = true) {
+ let me = this;
+ me.picker?.hide();
+
+ let tag = me.tagEl().innerHTML;
+ if (!me.addTag) {
+ if (tag === '') {
+ // "delete" ourselves
+ me.setVisible(false);
+ }
+ me.tag = tag;
+ me.updateColor(me.tag);
+ }
+
+ if (update) {
+ me.fireEvent('change', tag);
+ }
+
+ me.tagEl().contentEditable = false;
+ me.setMode('editable');
+ },
+
+ cancelEdit: function(list, event) {
+ let me = this;
+ if (me.mode === 'edit') {
+ me.setText(me.tag);
+ me.finishEdit(false);
+ }
+ },
+
+ setText: function(text) {
+ let me = this;
+ me.tagEl().innerHTML = text;
+ me.layoutCallback();
+ },
+
+ getTag: function() {
+ return this.tagEl().innerHTML;
+ },
+
+ setMode: function(mode) {
+ let me = this;
+ let icon = me.icons[me.addTag ? 'addTag' : mode];
+ let iconStyle = 'cursor: pointer; padding-right: 6px;';
+ let text = me.tag;
+ let cursor = 'pointer';
+ let padding = '0px';
+ switch (mode) {
+ case 'normal':
+ iconStyle += 'display: none;';
+ padding = undefined;
+ break;
+ case 'editable':
+ break;
+ case 'edit':
+ me.tagEl().contentEditable = true;
+ text = '';
+ cursor = undefined;
+ break;
+ default: return;
+ }
+
+ if (me.addTag) {
+ me.setText(text);
+ me.setStyle('cursor', cursor);
+ }
+
+ me.setStyle('padding-right', padding);
+
+ me.iconEl().classList = `fa fa-${icon}${me.faIconStyle}`;
+ me.iconEl().style = iconStyle;
+ me.mode = mode;
+ },
+
+ onKeyPress: function(event) {
+ let me = this;
+ let key = event.browserEvent.key;
+ if (key === "Enter") {
+ if (!me.addTag || me.tagEl().innerHTML !== '') {
+ me.finishEdit();
+ return;
+ }
+ } else if (key.match(PVE.Utils.tagCharRegex)) {
+ return;
+ }
+ event.browserEvent.preventDefault();
+ event.browserEvent.stopPropagation();
+ },
+
+ onInput: function() {
+ let me = this;
+ let tag = me.tagEl().innerHTML;
+ me.layoutCallback();
+ me.picker.getStore().filter({
+ property: 'tag',
+ value: tag,
+ });
+ },
+
+ listeners: {
+ mousedown: 'onMouseDown',
+ click: 'onClick',
+ focusleave: 'cancelEdit',
+ keypress: 'onKeyPress',
+ input: 'onInput',
+ element: 'el',
+ },
+
+ updateColor: function(tag) {
+ let me = this;
+ let rgb = PVE.Utils.tagOverrides[tag] ?? Proxmox.Utils.stringToRGB(tag);
+
+ let cls = Proxmox.Utils.getTextContrastClass(rgb);
+ let color = Proxmox.Utils.rgbToCss(rgb);
+ me.setUserCls(`proxmox-tag-${cls}`);
+ me.setStyle('background-color', color);
+ if (rgb.length > 3) {
+ let fgcolor = Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]]);
+ me.setStyle('color', fgcolor);
+ } else {
+ me.setStyle('color');
+ }
+ },
+
+ tagEl: function() {
+ return this.el?.dom?.children?.[0];
+ },
+
+ iconEl: function() {
+ return this.el?.dom?.children?.[1];
+ },
+
+ initComponent: function() {
+ let me = this;
+ if (me.tag === undefined && !me.addTag) {
+ throw "no tag given";
+ }
+ me.mode = me.mode ?? 'normal';
+
+ if (me.addTag) {
+ me.tag = gettext('Add Tag');
+ me.mode = 'editable';
+ me.setUserCls(`proxmox-tag-dark`);
+ }
+
+ let iconStyle = me.mode !== 'editable' ? 'display: none' : 'padding-right: 6px;';
+ let iconCls = me.icons[me.addTag ? 'addTag' : me.mode];
+
+ let icon = ` <i style="cursor: pointer; ${iconStyle}" class="fa fa-${iconCls}${me.faIconStyle}"></i>`;
+ me.html = `<span style="padding-right: 1px">${me.tag}</span>${icon}`;
+
+ me.callParent();
+ if (me.addTag) {
+ me.setStyle('cursor', 'pointer');
+ } else {
+ me.updateColor(me.tag);
+ }
+ me.setStyle('padding-right', me.mode === 'editable' ? '0px' : undefined);
+ },
+});
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 08/11] ui: add form/TagEdit.js
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (10 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 07/11] ui: add form/Tag Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 09/11] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
` (2 subsequent siblings)
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
this is a wrapper container for holding a list of (editable) tags
intended to be used in the lxc/qemu status toolbar
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/form/TagEdit.js | 132 +++++++++++++++++++++++++++++++++++
2 files changed, 133 insertions(+)
create mode 100644 www/manager6/form/TagEdit.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index b0729f7a..c95e9988 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -75,6 +75,7 @@ JSSRC= \
form/iScsiProviderSelector.js \
form/TagColorGrid.js \
form/Tag.js \
+ form/TagEdit.js \
grid/BackupView.js \
grid/FirewallAliases.js \
grid/FirewallOptions.js \
diff --git a/www/manager6/form/TagEdit.js b/www/manager6/form/TagEdit.js
new file mode 100644
index 00000000..d39225fb
--- /dev/null
+++ b/www/manager6/form/TagEdit.js
@@ -0,0 +1,132 @@
+Ext.define('PVE.panel.TagEditContainer', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.pveTagEditContainer',
+
+ tagCount: 0,
+
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ },
+
+ loadTags: function(tagstring = '') {
+ let me = this;
+
+ if (me.oldTags === tagstring) {
+ return;
+ }
+
+ let tags = tagstring.split(/[;, ]/).filter((t) => !!t) || [];
+ me.suspendLayout = true;
+ me.tags = {};
+ me.removeAllTags();
+ tags.forEach((tag) => {
+ me.addTag(tag);
+ });
+ me.suspendLayout = false;
+ me.updateLayout();
+ me.oldTags = tagstring;
+ },
+
+ getEditBtnHtml: function() {
+ let me = this;
+ let cls = '';
+ let qtip = '';
+ if (me.editMode) {
+ qtip = gettext('Apply Changes');
+ cls = 'check';
+ } else {
+ qtip = gettext('Edit Tags');
+ cls = 'pencil';
+ }
+ return ` <i data-qtip="${qtip}" class="fa fa-${cls}"></i>`;
+ },
+
+ onEditClick: function() {
+ let me = this;
+ me.editMode = !me.editMode;
+ let tagCount = 0;
+ me.tagFields.forEach((tag) => {
+ tag.setMode(me.editMode ? 'editable' : 'normal');
+ if (tag.isVisible() && !tag.addTag) {
+ tagCount++;
+ }
+ });
+
+ me.addTagBtn.setVisible(me.editMode);
+ me.editBtn.setHtml(me.getEditBtnHtml());
+ me.noTagsField.setVisible(!me.editMode && tagCount === 0);
+
+ if (!me.editMode) {
+ let tags = [];
+ me.tagFields.forEach((tag) => {
+ let tagValue = tag.getTag();
+ if (tag.isVisible() && tagValue) {
+ tags.push(tagValue);
+ }
+ });
+ tags = tags.join(',');
+ if (me.oldTags !== tags) {
+ me.oldTags = tags;
+ me.fireEvent('change', tags);
+ }
+ }
+ me.updateLayout();
+ },
+
+ removeAllTags: function() {
+ let me = this;
+ me.tagFields.forEach((tag) => {
+ me.remove(tag);
+ });
+ me.tagFields = [];
+ me.noTagsField.setVisible(true);
+ },
+
+ addTag: function(tag, inEdit) {
+ let me = this;
+ let tagField = me.insert(me.tagFields.length + 1, {
+ xtype: 'pmxTag',
+ tag,
+ mode: inEdit ? 'editable' : 'normal',
+ layoutCallback: () => me.updateLayout(),
+ });
+ me.tagFields.push(tagField);
+ me.noTagsField.setVisible(false);
+ },
+
+ initComponent: function() {
+ let me = this;
+ me.tagFields = [];
+ me.callParent();
+ me.noTagsField = me.add({
+ xtype: 'box',
+ html: gettext('No Tags'),
+ });
+ me.addTagBtn = me.add({
+ xtype: 'pmxTag',
+ addTag: true,
+ hidden: true,
+ layoutCallback: () => me.updateLayout(),
+ listeners: {
+ change: function(tag) {
+ me.addTag(tag, true);
+ },
+ },
+ });
+ me.editBtn = me.add({
+ xtype: 'box',
+ html: me.getEditBtnHtml(),
+ style: {
+ cursor: 'pointer',
+ },
+ listeners: {
+ click: () => me.onEditClick(),
+ element: 'el',
+ },
+ });
+ if (me.tags) {
+ me.loadTags(me.tags);
+ }
+ },
+});
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 09/11] ui: {lxc, qemu}/Config: show Tags and make them editable
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (11 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 08/11] ui: add form/TagEdit.js Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 10/11] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 11/11] ui: form/GlobalSearchField: display tags and allow to search for them Dominik Csapak
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
add the tags in the status line, and add a button for adding new ones
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/lxc/Config.js | 32 ++++++++++++++++++++++++++++++--
www/manager6/qemu/Config.js | 31 +++++++++++++++++++++++++++++--
2 files changed, 59 insertions(+), 4 deletions(-)
diff --git a/www/manager6/lxc/Config.js b/www/manager6/lxc/Config.js
index 89b59c9b..9f1994d3 100644
--- a/www/manager6/lxc/Config.js
+++ b/www/manager6/lxc/Config.js
@@ -4,6 +4,8 @@ Ext.define('PVE.lxc.Config', {
onlineHelp: 'chapter_pct',
+ userCls: 'proxmox-tags-full',
+
initComponent: function() {
var me = this;
var vm = me.pveSelNode.data;
@@ -182,12 +184,33 @@ Ext.define('PVE.lxc.Config', {
],
});
+ let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
+ tags: vm.tags,
+ listeners: {
+ change: function(tags) {
+ Proxmox.Utils.API2Request({
+ url: base_url + '/config',
+ method: 'PUT',
+ params: {
+ tags,
+ },
+ success: function() {
+ me.statusStore.load();
+ },
+ failure: function(response) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ me.statusStore.load();
+ },
+ });
+ },
+ },
+ });
Ext.apply(me, {
title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename),
hstateid: 'lxctab',
tbarSpacing: false,
- tbar: [statusTxt, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
+ tbar: [statusTxt, tagsContainer, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
defaults: { statusStore: me.statusStore },
items: [
{
@@ -344,10 +367,12 @@ Ext.define('PVE.lxc.Config', {
me.mon(me.statusStore, 'load', function(s, records, success) {
var status;
var lock;
+ var rec;
+
if (!success) {
status = 'unknown';
} else {
- var rec = s.data.get('status');
+ rec = s.data.get('status');
status = rec ? rec.data.value : 'unknown';
rec = s.data.get('template');
template = rec ? rec.data.value : false;
@@ -357,6 +382,9 @@ Ext.define('PVE.lxc.Config', {
statusTxt.update({ lock: lock });
+ rec = s.data.get('tags');
+ tagsContainer.loadTags(rec?.data?.value);
+
startBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status === 'running' || template);
shutdownBtn.setDisabled(!caps.vms['VM.PowerMgmt'] || status !== 'running');
me.down('#removeBtn').setDisabled(!caps.vms['VM.Allocate'] || status !== 'stopped');
diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js
index 9fe933df..2cd6d856 100644
--- a/www/manager6/qemu/Config.js
+++ b/www/manager6/qemu/Config.js
@@ -3,6 +3,7 @@ Ext.define('PVE.qemu.Config', {
alias: 'widget.PVE.qemu.Config',
onlineHelp: 'chapter_virtual_machines',
+ userCls: 'proxmox-tags-full',
initComponent: function() {
var me = this;
@@ -219,11 +220,33 @@ Ext.define('PVE.qemu.Config', {
],
});
+ let tagsContainer = Ext.create('PVE.panel.TagEditContainer', {
+ tags: vm.tags,
+ listeners: {
+ change: function(tags) {
+ Proxmox.Utils.API2Request({
+ url: base_url + '/config',
+ method: 'PUT',
+ params: {
+ tags,
+ },
+ success: function() {
+ me.statusStore.load();
+ },
+ failure: function(response) {
+ Ext.Msg.alert('Error', response.htmlStatus);
+ me.statusStore.load();
+ },
+ });
+ },
+ },
+ });
+
Ext.apply(me, {
title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename),
hstateid: 'kvmtab',
tbarSpacing: false,
- tbar: [statusTxt, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
+ tbar: [statusTxt, tagsContainer, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
defaults: { statusStore: me.statusStore },
items: [
{
@@ -382,11 +405,12 @@ Ext.define('PVE.qemu.Config', {
var spice = false;
var xtermjs = false;
var lock;
+ var rec;
if (!success) {
status = qmpstatus = 'unknown';
} else {
- var rec = s.data.get('status');
+ rec = s.data.get('status');
status = rec ? rec.data.value : 'unknown';
rec = s.data.get('qmpstatus');
qmpstatus = rec ? rec.data.value : 'unknown';
@@ -399,6 +423,9 @@ Ext.define('PVE.qemu.Config', {
xtermjs = !!s.data.get('serial');
}
+ rec = s.data.get('tags');
+ tagsContainer.loadTags(rec?.data?.value);
+
if (template) {
return;
}
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 10/11] ui: tree/ResourceTree: show Tags in tree
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (12 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 09/11] ui: {lxc, qemu}/Config: show Tags and make them editable Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 11/11] ui: form/GlobalSearchField: display tags and allow to search for them Dominik Csapak
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/lxc/Config.js | 4 +++-
www/manager6/qemu/Config.js | 4 +++-
www/manager6/tree/ResourceTree.js | 4 ++++
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/www/manager6/lxc/Config.js b/www/manager6/lxc/Config.js
index 9f1994d3..1b79628e 100644
--- a/www/manager6/lxc/Config.js
+++ b/www/manager6/lxc/Config.js
@@ -206,8 +206,10 @@ Ext.define('PVE.lxc.Config', {
},
});
+ let vm_text = `${vm.vmid} (${vm.name})`;
+
Ext.apply(me, {
- title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm.text, nodename),
+ title: Ext.String.format(gettext("Container {0} on node '{1}'"), vm_text, nodename),
hstateid: 'lxctab',
tbarSpacing: false,
tbar: [statusTxt, tagsContainer, '->', startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
diff --git a/www/manager6/qemu/Config.js b/www/manager6/qemu/Config.js
index 2cd6d856..5c8fa620 100644
--- a/www/manager6/qemu/Config.js
+++ b/www/manager6/qemu/Config.js
@@ -242,8 +242,10 @@ Ext.define('PVE.qemu.Config', {
},
});
+ let vm_text = `${vm.vmid} (${vm.name})`;
+
Ext.apply(me, {
- title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm.text, nodename),
+ title: Ext.String.format(gettext("Virtual Machine {0} on node '{1}'"), vm_text, nodename),
hstateid: 'kvmtab',
tbarSpacing: false,
tbar: [statusTxt, tagsContainer, '->', resumeBtn, startBtn, shutdownBtn, migrateBtn, consoleBtn, moreBtn],
diff --git a/www/manager6/tree/ResourceTree.js b/www/manager6/tree/ResourceTree.js
index 139defab..d41721b9 100644
--- a/www/manager6/tree/ResourceTree.js
+++ b/www/manager6/tree/ResourceTree.js
@@ -5,6 +5,8 @@ Ext.define('PVE.tree.ResourceTree', {
extend: 'Ext.tree.TreePanel',
alias: ['widget.pveResourceTree'],
+ userCls: 'proxmox-tags-circle',
+
statics: {
typeDefaults: {
node: {
@@ -114,6 +116,8 @@ Ext.define('PVE.tree.ResourceTree', {
}
}
+ info.text += PVE.Utils.renderTags(info.tags, PVE.Utils.tagOverrides);
+
info.text = status + info.text;
},
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pve-devel] [PATCH manager v6 11/11] ui: form/GlobalSearchField: display tags and allow to search for them
2022-04-12 13:34 [pve-devel] [PATCH cluster/widget-toolkit/manager v6] add tags to ui Dominik Csapak
` (13 preceding siblings ...)
2022-04-12 13:34 ` [pve-devel] [PATCH manager v6 10/11] ui: tree/ResourceTree: show Tags in tree Dominik Csapak
@ 2022-04-12 13:34 ` Dominik Csapak
14 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2022-04-12 13:34 UTC (permalink / raw)
To: pve-devel
each tag is treated like a seperate field, so it weighs more if the user
searches for the exact string of a single tag
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/form/GlobalSearchField.js | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/www/manager6/form/GlobalSearchField.js b/www/manager6/form/GlobalSearchField.js
index 267a480d..8e815d4f 100644
--- a/www/manager6/form/GlobalSearchField.js
+++ b/www/manager6/form/GlobalSearchField.js
@@ -15,6 +15,7 @@ Ext.define('PVE.form.GlobalSearchField', {
grid: {
xtype: 'gridpanel',
+ userCls: 'proxmox-tags-full',
focusOnToFront: false,
floating: true,
emptyText: Proxmox.Utils.noneText,
@@ -23,7 +24,7 @@ Ext.define('PVE.form.GlobalSearchField', {
scrollable: {
xtype: 'scroller',
y: true,
- x: false,
+ x: true,
},
store: {
model: 'PVEResources',
@@ -78,6 +79,11 @@ Ext.define('PVE.form.GlobalSearchField', {
text: gettext('Description'),
flex: 1,
dataIndex: 'text',
+ renderer: function(value, mD, rec) {
+ let overrides = PVE.Utils.tagOverrides;
+ let tags = PVE.Utils.renderTags(rec.data.tags, overrides);
+ return `${value}${tags}`;
+ },
},
{
text: gettext('Node'),
@@ -104,16 +110,20 @@ Ext.define('PVE.form.GlobalSearchField', {
'storage': ['type', 'pool', 'node', 'storage'],
'default': ['name', 'type', 'node', 'pool', 'vmid'],
};
- let fieldArr = fieldMap[item.data.type] || fieldMap.default;
+ let fields = fieldMap[item.data.type] || fieldMap.default;
+ let fieldArr = fields.map(field => item.data[field]?.toString().toLowerCase());
+ if (item.data.tags) {
+ let tags = item.data.tags.split(/[;, ]/);
+ fieldArr.push(...tags);
+ }
let filterWords = me.filterVal.split(/\s+/);
// all text is case insensitive and each split-out word is searched for separately.
// a row gets 1 point for every partial match, and and additional point for every exact match
let match = 0;
- for (let field of fieldArr) {
- let fieldValue = item.data[field]?.toString().toLowerCase();
- if (fieldValue === undefined) {
+ for (let fieldValue of fieldArr) {
+ if (fieldValue === undefined || fieldValue === "") {
continue;
}
for (let filterWord of filterWords) {
--
2.30.2
^ permalink raw reply [flat|nested] 18+ messages in thread