From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 079E0846E for ; Tue, 15 Nov 2022 14:03:52 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 1225D2EDB for ; Tue, 15 Nov 2022 14:03:05 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Tue, 15 Nov 2022 14:02:59 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id A1A7744C95 for ; Tue, 15 Nov 2022 14:02:52 +0100 (CET) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Tue, 15 Nov 2022 14:02:40 +0100 Message-Id: <20221115130248.1007325-16-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20221115130248.1007325-1-d.csapak@proxmox.com> References: <20221115130248.1007325-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: =?UTF-8?Q?0=0A=09?=AWL 0.065 Adjusted score from AWL reputation of From: =?UTF-8?Q?address=0A=09?=BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict =?UTF-8?Q?Alignment=0A=09?=SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF =?UTF-8?Q?Record=0A=09?=SPF_PASS -0.001 SPF: sender matches SPF =?UTF-8?Q?record=0A=09?=URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [picker.style] Subject: [pve-devel] [PATCH manager v10 05/13] ui: add form/TagColorGrid X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 15 Nov 2022 13:03:52 -0000 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 --- 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 4ff8a1a55..7cb77083d 100644 --- a/www/manager6/Utils.js +++ b/www/manager6/Utils.js @@ -1918,6 +1918,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