--- /dev/null
+// Override some components from widget toolkit.
+// This was done so that we can already use the improved UI for editing
+// match rules without waiting for the needed API calls in PVE to be merged
+//
+// This can and *should* be removed once these changes have landed in
+// widget toolkit:
+// https://lists.proxmox.com/pipermail/pve-devel/2024-April/063539.html
+
+
+Ext.define('pbs-notification-fields', {
+ extend: 'Ext.data.Model',
+ fields: ['name', 'description'],
+ idProperty: 'name',
+});
+
+Ext.define('pbs-notification-field-values', {
+ extend: 'Ext.data.Model',
+ fields: ['value', 'comment', 'field'],
+ idProperty: 'value',
+});
+
+Ext.define('PBS.panel.NotificationRulesEditPanel', {
+ override: 'Proxmox.panel.NotificationRulesEditPanel',
+ extend: 'Proxmox.panel.InputPanel',
+ xtype: 'pmxNotificationMatchRulesEditPanel',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ // we want to also set the empty value, but 'bind' does not do that so
+ // we have to set it then (and only then) to get the correct value in
+ // the tree
+ control: {
+ 'field': {
+ change: function(cmp) {
+ let me = this;
+ let vm = me.getViewModel();
+ if (cmp.field) {
+ let record = vm.get('selectedRecord');
+ if (!record) {
+ return;
+ }
+ let data = Ext.apply({}, record.get('data'));
+ let value = cmp.getValue();
+ // only update if the value is empty (or empty array)
+ if (!value || !value.length) {
+ data[cmp.field] = value;
+ record.set({ data });
+ }
+ }
+ },
+ },
+ },
+ },
+
+ viewModel: {
+ data: {
+ selectedRecord: null,
+ matchFieldType: 'exact',
+ matchFieldField: '',
+ matchFieldValue: '',
+ rootMode: 'all',
+ },
+
+ formulas: {
+ nodeType: {
+ get: function(get) {
+ let record = get('selectedRecord');
+ return record?.get('type');
+ },
+ set: function(value) {
+ let me = this;
+ let record = me.get('selectedRecord');
+
+ let data;
+
+ switch (value) {
+ case 'match-severity':
+ data = {
+ value: ['info', 'notice', 'warning', 'error', 'unknown'],
+ };
+ break;
+ case 'match-field':
+ data = {
+ type: 'exact',
+ field: '',
+ value: '',
+ };
+ break;
+ case 'match-calendar':
+ data = {
+ value: '',
+ };
+ break;
+ }
+
+ let node = {
+ type: value,
+ data,
+ };
+ record.set(node);
+ },
+ },
+ showMatchingMode: function(get) {
+ let record = get('selectedRecord');
+ if (!record) {
+ return false;
+ }
+ return record.isRoot();
+ },
+ showMatcherType: function(get) {
+ let record = get('selectedRecord');
+ if (!record) {
+ return false;
+ }
+ return !record.isRoot();
+ },
+
+ rootMode: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ set: function(value) {
+ let me = this;
+ let record = me.get('selectedRecord');
+ let currentData = record.get('data');
+ let invert = false;
+ if (value.startsWith('not')) {
+ value = value.substring(3);
+ invert = true;
+ }
+ record.set({
+ data: {
+ ...currentData,
+ value,
+ invert,
+ },
+ });
+ },
+ get: function(record) {
+ let prefix = record?.get('data').invert ? 'not' : '';
+ return prefix + record?.get('data')?.value;
+ },
+ },
+ },
+ },
+
+ column1: [
+ {
+ xtype: 'pbsNotificationMatchRuleTree',
+ cbind: {
+ isCreate: '{isCreate}',
+ },
+ },
+ ],
+ column2: [
+ {
+ xtype: 'pbsNotificationMatchRuleSettings',
+ cbind: {
+ baseUrl: '{baseUrl}',
+ },
+ },
+
+ ],
+
+ onGetValues: function(values) {
+ let me = this;
+
+ let deleteArrayIfEmtpy = (field) => {
+ if (Ext.isArray(values[field])) {
+ if (values[field].length === 0) {
+ delete values[field];
+ if (!me.isCreate) {
+ Proxmox.Utils.assemble_field_data(values, { 'delete': field });
+ }
+ }
+ }
+ };
+ deleteArrayIfEmtpy('match-field');
+ deleteArrayIfEmtpy('match-severity');
+ deleteArrayIfEmtpy('match-calendar');
+
+ return values;
+ },
+});
+
+Ext.define('PBS.panel.NotificationMatchRuleTree', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pbsNotificationMatchRuleTree',
+ mixins: ['Proxmox.Mixin.CBind'],
+ border: false,
+
+ getNodeTextAndIcon: function(type, data) {
+ let text;
+ let iconCls;
+
+ switch (type) {
+ case 'match-severity': {
+ let v = data.value;
+ if (Ext.isArray(data.value)) {
+ v = data.value.join(', ');
+ }
+ text = Ext.String.format(gettext("Match severity: {0}"), v);
+ iconCls = 'fa fa-exclamation';
+ if (!v) {
+ iconCls += ' internal-error';
+ }
+ } break;
+ case 'match-field': {
+ let field = data.field;
+ let value = data.value;
+ text = Ext.String.format(gettext("Match field: {0}={1}"), field, value);
+ iconCls = 'fa fa-square-o';
+ if (!field || !value || (Ext.isArray(value) && !value.length)) {
+ iconCls += ' internal-error';
+ }
+ } break;
+ case 'match-calendar': {
+ let v = data.value;
+ text = Ext.String.format(gettext("Match calendar: {0}"), v);
+ iconCls = 'fa fa-calendar-o';
+ if (!v || !v.length) {
+ iconCls += ' internal-error';
+ }
+ } break;
+ case 'mode':
+ if (data.value === 'all') {
+ text = gettext("All");
+ } else if (data.value === 'any') {
+ text = gettext("Any");
+ }
+ if (data.invert) {
+ text = `!${text}`;
+ }
+ iconCls = 'fa fa-filter';
+
+ break;
+ }
+
+ return [text, iconCls];
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let treeStore = Ext.create('Ext.data.TreeStore', {
+ root: {
+ expanded: true,
+ expandable: false,
+ text: '',
+ type: 'mode',
+ data: {
+ value: 'all',
+ invert: false,
+ },
+ children: [],
+ iconCls: 'fa fa-filter',
+ },
+ });
+
+ let realMatchFields = Ext.create({
+ xtype: 'hiddenfield',
+ setValue: function(value) {
+ this.value = value;
+ this.checkChange();
+ },
+ getValue: function() {
+ return this.value;
+ },
+ getErrors: function() {
+ for (const matcher of this.value ?? []) {
+ let matches = matcher.match(/^([^:]+):([^=]+)=(.+)$/);
+ if (!matches) {
+ return [""]; // fake error for validation
+ }
+ }
+ return [];
+ },
+ getSubmitValue: function() {
+ let value = this.value;
+ if (!value) {
+ value = [];
+ }
+ return value;
+ },
+ name: 'match-field',
+ });
+
+ let realMatchSeverity = Ext.create({
+ xtype: 'hiddenfield',
+ setValue: function(value) {
+ this.value = value;
+ this.checkChange();
+ },
+ getValue: function() {
+ return this.value;
+ },
+ getErrors: function() {
+ for (const severities of this.value ?? []) {
+ if (!severities) {
+ return [""]; // fake error for validation
+ }
+ }
+ return [];
+ },
+ getSubmitValue: function() {
+ let value = this.value;
+ if (!value) {
+ value = [];
+ }
+ return value;
+ },
+ name: 'match-severity',
+ });
+
+ let realMode = Ext.create({
+ xtype: 'hiddenfield',
+ name: 'mode',
+ setValue: function(value) {
+ this.value = value;
+ this.checkChange();
+ },
+ getValue: function() {
+ return this.value;
+ },
+ getSubmitValue: function() {
+ let value = this.value;
+ return value;
+ },
+ });
+
+ let realMatchCalendar = Ext.create({
+ xtype: 'hiddenfield',
+ name: 'match-calendar',
+
+ setValue: function(value) {
+ this.value = value;
+ this.checkChange();
+ },
+ getValue: function() {
+ return this.value;
+ },
+ getErrors: function() {
+ for (const timespan of this.value ?? []) {
+ if (!timespan) {
+ return [""]; // fake error for validation
+ }
+ }
+ return [];
+ },
+ getSubmitValue: function() {
+ let value = this.value;
+ return value;
+ },
+ });
+
+ let realInvertMatch = Ext.create({
+ xtype: 'proxmoxcheckbox',
+ name: 'invert-match',
+ hidden: true,
+ deleteEmpty: !me.isCreate,
+ });
+
+ let storeChanged = function(store) {
+ store.suspendEvent('datachanged');
+
+ let matchFieldStmts = [];
+ let matchSeverityStmts = [];
+ let matchCalendarStmts = [];
+ let modeStmt = 'all';
+ let invertMatchStmt = false;
+
+ store.each(function(model) {
+ let type = model.get('type');
+ let data = model.get('data');
+
+ switch (type) {
+ case 'match-field':
+ matchFieldStmts.push(`${data.type}:${data.field ?? ''}=${data.value ?? ''}`);
+ break;
+ case 'match-severity':
+ if (Ext.isArray(data.value)) {
+ matchSeverityStmts.push(data.value.join(','));
+ } else {
+ matchSeverityStmts.push(data.value);
+ }
+ break;
+ case 'match-calendar':
+ matchCalendarStmts.push(data.value);
+ break;
+ case 'mode':
+ modeStmt = data.value;
+ invertMatchStmt = data.invert;
+ break;
+ }
+
+ let [text, iconCls] = me.getNodeTextAndIcon(type, data);
+ model.set({
+ text,
+ iconCls,
+ });
+ });
+
+ realMatchFields.suspendEvent('change');
+ realMatchFields.setValue(matchFieldStmts);
+ realMatchFields.resumeEvent('change');
+
+ realMatchCalendar.suspendEvent('change');
+ realMatchCalendar.setValue(matchCalendarStmts);
+ realMatchCalendar.resumeEvent('change');
+
+ realMode.suspendEvent('change');
+ realMode.setValue(modeStmt);
+ realMode.resumeEvent('change');
+
+ realInvertMatch.suspendEvent('change');
+ realInvertMatch.setValue(invertMatchStmt);
+ realInvertMatch.resumeEvent('change');
+
+ realMatchSeverity.suspendEvent('change');
+ realMatchSeverity.setValue(matchSeverityStmts);
+ realMatchSeverity.resumeEvent('change');
+
+ store.resumeEvent('datachanged');
+ };
+
+ realMatchFields.addListener('change', function(field, value) {
+ let parseMatchField = function(filter) {
+ let [, type, matchedField, matchedValue] =
+ filter.match(/^(?:(regex|exact):)?([A-Za-z0-9_][A-Za-z0-9._-]*)=(.+)$/);
+ if (type === undefined) {
+ type = "exact";
+ }
+
+ if (type === 'exact') {
+ matchedValue = matchedValue.split(',');
+ }
+
+ return {
+ type: 'match-field',
+ data: {
+ type,
+ field: matchedField,
+ value: matchedValue,
+ },
+ leaf: true,
+ };
+ };
+
+ for (let node of treeStore.queryBy(
+ record => record.get('type') === 'match-field',
+ ).getRange()) {
+ node.remove(true);
+ }
+
+ if (!value) {
+ return;
+ }
+ let records = value.map(parseMatchField);
+
+ let rootNode = treeStore.getRootNode();
+
+ for (let record of records) {
+ rootNode.appendChild(record);
+ }
+ });
+
+ realMatchSeverity.addListener('change', function(field, value) {
+ let parseSeverity = function(severities) {
+ return {
+ type: 'match-severity',
+ data: {
+ value: severities.split(','),
+ },
+ leaf: true,
+ };
+ };
+
+ for (let node of treeStore.queryBy(
+ record => record.get('type') === 'match-severity').getRange()) {
+ node.remove(true);
+ }
+
+ let records = value.map(parseSeverity);
+ let rootNode = treeStore.getRootNode();
+
+ for (let record of records) {
+ rootNode.appendChild(record);
+ }
+ });
+
+ realMatchCalendar.addListener('change', function(field, value) {
+ let parseCalendar = function(timespan) {
+ return {
+ type: 'match-calendar',
+ data: {
+ value: timespan,
+ },
+ leaf: true,
+ };
+ };
+
+ for (let node of treeStore.queryBy(
+ record => record.get('type') === 'match-calendar').getRange()) {
+ node.remove(true);
+ }
+
+ let records = value.map(parseCalendar);
+ let rootNode = treeStore.getRootNode();
+
+ for (let record of records) {
+ rootNode.appendChild(record);
+ }
+ });
+
+ realMode.addListener('change', function(field, value) {
+ let data = treeStore.getRootNode().get('data');
+ treeStore.getRootNode().set('data', {
+ ...data,
+ value,
+ });
+ });
+
+ realInvertMatch.addListener('change', function(field, value) {
+ let data = treeStore.getRootNode().get('data');
+ treeStore.getRootNode().set('data', {
+ ...data,
+ invert: value,
+ });
+ });
+
+ treeStore.addListener('datachanged', storeChanged);
+
+ let treePanel = Ext.create({
+ xtype: 'treepanel',
+ store: treeStore,
+ minHeight: 300,
+ maxHeight: 300,
+ scrollable: true,
+
+ bind: {
+ selection: '{selectedRecord}',
+ },
+ });
+
+ let addNode = function() {
+ let node = {
+ type: 'match-field',
+ data: {
+ type: 'exact',
+ field: '',
+ value: '',
+ },
+ leaf: true,
+ };
+ treeStore.getRootNode().appendChild(node);
+ treePanel.setSelection(treeStore.getRootNode().lastChild);
+ };
+
+ let deleteNode = function() {
+ let selection = treePanel.getSelection();
+ for (let selected of selection) {
+ if (!selected.isRoot()) {
+ selected.remove(true);
+ }
+ }
+ };
+
+ Ext.apply(me, {
+ items: [
+ realMatchFields,
+ realMode,
+ realMatchSeverity,
+ realInvertMatch,
+ realMatchCalendar,
+ treePanel,
+ {
+ xtype: 'button',
+ margin: '5 5 5 0',
+ text: gettext('Add'),
+ iconCls: 'fa fa-plus-circle',
+ handler: addNode,
+ },
+ {
+ xtype: 'button',
+ margin: '5 5 5 0',
+ text: gettext('Remove'),
+ iconCls: 'fa fa-minus-circle',
+ handler: deleteNode,
+ },
+ ],
+ });
+ me.callParent();
+ },
+});
+
+Ext.define('PBS.panel.NotificationMatchRuleSettings', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pbsNotificationMatchRuleSettings',
+ mixins: ['Proxmox.Mixin.CBind'],
+ border: false,
+ layout: 'anchor',
+
+ items: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ name: 'mode',
+ fieldLabel: gettext('Match if'),
+ allowBlank: false,
+ isFormField: false,
+
+ matchFieldWidth: false,
+
+ comboItems: [
+ ['all', gettext('All rules match')],
+ ['any', gettext('Any rule matches')],
+ ['notall', gettext('At least one rule does not match')],
+ ['notany', gettext('No rule matches')],
+ ],
+ // Hide initially to avoid glitches when opening the window
+ hidden: true,
+ bind: {
+ hidden: '{!showMatchingMode}',
+ disabled: '{!showMatchingMode}',
+ value: '{rootMode}',
+ },
+ },
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Node type'),
+ isFormField: false,
+ allowBlank: false,
+ // Hide initially to avoid glitches when opening the window
+ hidden: true,
+ bind: {
+ value: '{nodeType}',
+ hidden: '{!showMatcherType}',
+ disabled: '{!showMatcherType}',
+ },
+
+ comboItems: [
+ ['match-field', gettext('Match Field')],
+ ['match-severity', gettext('Match Severity')],
+ ['match-calendar', gettext('Match Calendar')],
+ ],
+ },
+ {
+ xtype: 'pbsNotificationMatchFieldSettings',
+ cbind: {
+ baseUrl: '{baseUrl}',
+ },
+ },
+ {
+ xtype: 'pbsNotificationMatchSeveritySettings',
+ },
+ {
+ xtype: 'pbsNotificationMatchCalendarSettings',
+ },
+ ],
+});
+
+Ext.define('PBS.panel.MatchCalendarSettings', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pbsNotificationMatchCalendarSettings',
+ border: false,
+ layout: 'anchor',
+ // Hide initially to avoid glitches when opening the window
+ hidden: true,
+ bind: {
+ hidden: '{!typeIsMatchCalendar}',
+ },
+ viewModel: {
+ // parent is set in `initComponents`
+ formulas: {
+ typeIsMatchCalendar: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ get: function(record) {
+ return record?.get('type') === 'match-calendar';
+ },
+ },
+
+ matchCalendarValue: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ set: function(value) {
+ let me = this;
+ let record = me.get('selectedRecord');
+ let currentData = record.get('data');
+ record.set({
+ data: {
+ ...currentData,
+ value: value,
+ },
+ });
+ },
+ get: function(record) {
+ return record?.get('data')?.value;
+ },
+ },
+ },
+ },
+ items: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Timespan to match'),
+ isFormField: false,
+ allowBlank: false,
+ editable: true,
+ displayField: 'key',
+ field: 'value',
+ bind: {
+ value: '{matchCalendarValue}',
+ disabled: '{!typeIsMatchCalender}',
+ },
+
+ comboItems: [
+ ['mon 8-12', ''],
+ ['tue..fri,sun 0:00-23:59', ''],
+ ],
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+ Ext.apply(me.viewModel, {
+ parent: me.up('pmxNotificationMatchRulesEditPanel').getViewModel(),
+ });
+ me.callParent();
+ },
+});
+
+Ext.define('PBS.panel.MatchSeveritySettings', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pbsNotificationMatchSeveritySettings',
+ border: false,
+ layout: 'anchor',
+ // Hide initially to avoid glitches when opening the window
+ hidden: true,
+ bind: {
+ hidden: '{!typeIsMatchSeverity}',
+ },
+ viewModel: {
+ // parent is set in `initComponents`
+ formulas: {
+ typeIsMatchSeverity: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ get: function(record) {
+ return record?.get('type') === 'match-severity';
+ },
+ },
+ matchSeverityValue: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ set: function(value) {
+ let record = this.get('selectedRecord');
+ let currentData = record.get('data');
+ record.set({
+ data: {
+ ...currentData,
+ value: value,
+ },
+ });
+ },
+ get: function(record) {
+ return record?.get('data')?.value;
+ },
+ },
+ },
+ },
+ items: [
+ {
+ xtype: 'proxmoxKVComboBox',
+ fieldLabel: gettext('Severities to match'),
+ isFormField: false,
+ allowBlank: true,
+ multiSelect: true,
+ field: 'value',
+ // Hide initially to avoid glitches when opening the window
+ hidden: true,
+ bind: {
+ value: '{matchSeverityValue}',
+ hidden: '{!typeIsMatchSeverity}',
+ disabled: '{!typeIsMatchSeverity}',
+ },
+
+ comboItems: [
+ ['info', gettext('Info')],
+ ['notice', gettext('Notice')],
+ ['warning', gettext('Warning')],
+ ['error', gettext('Error')],
+ ['unknown', gettext('Unknown')],
+ ],
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+ Ext.apply(me.viewModel, {
+ parent: me.up('pmxNotificationMatchRulesEditPanel').getViewModel(),
+ });
+ me.callParent();
+ },
+});
+
+Ext.define('PBS.panel.MatchFieldSettings', {
+ extend: 'Ext.panel.Panel',
+ xtype: 'pbsNotificationMatchFieldSettings',
+ border: false,
+ layout: 'anchor',
+ // Hide initially to avoid glitches when opening the window
+ hidden: true,
+ bind: {
+ hidden: '{!typeIsMatchField}',
+ },
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ control: {
+ 'field[reference=fieldSelector]': {
+ change: function(field) {
+ let view = this.getView();
+ let valueField = view.down('field[reference=valueSelector]');
+ let store = valueField.getStore();
+ let val = field.getValue();
+
+ if (val) {
+ store.setFilters([
+ {
+ property: 'field',
+ value: val,
+ },
+ ]);
+ }
+ },
+ },
+ },
+ },
+ viewModel: {
+ // parent is set in `initComponents`
+ formulas: {
+ typeIsMatchField: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ get: function(record) {
+ return record?.get('type') === 'match-field';
+ },
+ },
+ isRegex: function(get) {
+ return get('matchFieldType') === 'regex';
+ },
+ matchFieldType: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ set: function(value) {
+ let record = this.get('selectedRecord');
+ let currentData = record.get('data');
+
+ let newValue = [];
+
+ // Build equivalent regular expression if switching
+ // to 'regex' mode
+ if (value === 'regex') {
+ let regexVal = "^";
+ if (currentData.value) {
+ regexVal += `(${currentData.value.join('|')})`;
+ }
+ regexVal += "$";
+ newValue.push(regexVal);
+ }
+
+ record.set({
+ data: {
+ ...currentData,
+ type: value,
+ value: newValue,
+ },
+ });
+ },
+ get: function(record) {
+ return record?.get('data')?.type;
+ },
+ },
+ matchFieldField: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ set: function(value) {
+ let record = this.get('selectedRecord');
+ let currentData = record.get('data');
+
+ record.set({
+ data: {
+ ...currentData,
+ field: value,
+ // Reset value if field changes
+ value: [],
+ },
+ });
+ },
+ get: function(record) {
+ return record?.get('data')?.field;
+ },
+ },
+ matchFieldValue: {
+ bind: {
+ bindTo: '{selectedRecord}',
+ deep: true,
+ },
+ set: function(value) {
+ let record = this.get('selectedRecord');
+ let currentData = record.get('data');
+ record.set({
+ data: {
+ ...currentData,
+ value: value,
+ },
+ });
+ },
+ get: function(record) {
+ return record?.get('data')?.value;
+ },
+ },
+ },
+ },
+
+ initComponent: function() {
+ let me = this;
+
+ let store = Ext.create('Ext.data.Store', {
+ model: 'pbs-notification-fields',
+ autoLoad: true,
+ proxy: {
+ type: 'proxmox',
+ url: `/api2/json/${me.baseUrl}/matcher-fields`,
+ },
+ listeners: {
+ 'load': function() {
+ this.each(function(record) {
+ record.set({
+ description:
+ Proxmox.Utils.formatNotificationFieldName(
+ record.get('name'),
+ ),
+ });
+ });
+
+ // Commit changes so that the description field is not marked
+ // as dirty
+ this.commitChanges();
+ },
+ },
+ });
+
+ let valueStore = Ext.create('Ext.data.Store', {
+ model: 'pbs-notification-field-values',
+ autoLoad: true,
+ proxy: {
+ type: 'proxmox',
+
+ url: `/api2/json/${me.baseUrl}/matcher-field-values`,
+ },
+ listeners: {
+ 'load': function() {
+ this.each(function(record) {
+ if (record.get('field') === 'type') {
+ record.set({
+ comment:
+ Proxmox.Utils.formatNotificationFieldValue(
+ record.get('value'),
+ ),
+ });
+ }
+ }, this, true);
+
+ // Commit changes so that the description field is not marked
+ // as dirty
+ this.commitChanges();
+ },
+ },
+ });
+
+ Ext.apply(me.viewModel, {
+ parent: me.up('pmxNotificationMatchRulesEditPanel').getViewModel(),
+ });
+ Ext.apply(me, {
+ items: [
+ {
+ fieldLabel: gettext('Match Type'),
+ xtype: 'proxmoxKVComboBox',
+ reference: 'type',
+ isFormField: false,
+ allowBlank: false,
+ submitValue: false,
+ field: 'type',
+
+ bind: {
+ value: '{matchFieldType}',
+ },
+
+ comboItems: [
+ ['exact', gettext('Exact')],
+ ['regex', gettext('Regex')],
+ ],
+ },
+ {
+ fieldLabel: gettext('Field'),
+ reference: 'fieldSelector',
+ xtype: 'proxmoxComboGrid',
+ isFormField: false,
+ submitValue: false,
+ allowBlank: false,
+ editable: false,
+ store: store,
+ queryMode: 'local',
+ valueField: 'name',
+ displayField: 'description',
+ field: 'field',
+ bind: {
+ value: '{matchFieldField}',
+ },
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Description'),
+ dataIndex: 'description',
+ flex: 2,
+ },
+ {
+ header: gettext('Field Name'),
+ dataIndex: 'name',
+ flex: 1,
+ },
+ ],
+ },
+ },
+ {
+ fieldLabel: gettext('Value'),
+ reference: 'valueSelector',
+ xtype: 'proxmoxComboGrid',
+ autoSelect: false,
+ editable: false,
+ isFormField: false,
+ submitValue: false,
+ allowBlank: false,
+ showClearTrigger: true,
+ field: 'value',
+ store: valueStore,
+ valueField: 'value',
+ displayField: 'value',
+ notFoundIsValid: false,
+ multiSelect: true,
+ bind: {
+ value: '{matchFieldValue}',
+ hidden: '{isRegex}',
+ },
+ listConfig: {
+ columns: [
+ {
+ header: gettext('Value'),
+ dataIndex: 'value',
+ flex: 1,
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comment',
+ flex: 2,
+ },
+ ],
+ },
+ },
+ {
+ fieldLabel: gettext('Regex'),
+ xtype: 'proxmoxtextfield',
+ editable: true,
+ isFormField: false,
+ submitValue: false,
+ allowBlank: false,
+ field: 'value',
+ bind: {
+ value: '{matchFieldValue}',
+ hidden: '{!isRegex}',
+ },
+ },
+ ],
+ });
+ me.callParent();
+ },
+});