ui: add grid form for mapping tags to foreground/background colors

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>
This commit is contained in:
Dominik Csapak 2022-11-16 16:48:07 +01:00 committed by Thomas Lamprecht
parent 8b4c7a46e2
commit 7381265da2
4 changed files with 365 additions and 0 deletions

View File

@ -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;
}

View File

@ -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 \

View File

@ -1921,6 +1921,8 @@ Ext.define('PVE.Utils', {
PVE.data.ResourceStore.fireEvent('load');
Ext.GlobalEvents.fireEvent('loadedUiOptions');
},
tagCharRegex: /^[a-z0-9+_.-]$/i,
},
singleton: true,

View File

@ -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();
},
});