From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 917B41FF0E0 for ; Thu, 09 Jul 2026 14:00:10 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 652D621752; Thu, 09 Jul 2026 13:58:15 +0200 (CEST) From: Lukas Wagner To: pbs-devel@lists.proxmox.com, pve-devel@lists.proxmox.com Subject: [PATCH proxmox-widget-toolkit 18/29] notification: matcher: add better calendar editor Date: Thu, 9 Jul 2026 13:57:05 +0200 Message-ID: <20260709115716.299836-19-l.wagner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260709115716.299836-1-l.wagner@proxmox.com> References: <20260709115716.299836-1-l.wagner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1783598239766 X-SPAM-LEVEL: Spam detection results: 0 DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: VWJIMUCOZ4RGIMZ34WV6ROPB6SLGV2XL X-Message-ID-Hash: VWJIMUCOZ4RGIMZ34WV6ROPB6SLGV2XL X-MailFrom: l.wagner@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This new editor allows one to enter the start time, end time and tick the matched week-days, instead of having to enter the appropriate string representation of the time range (e.g. 'mon..tue 08:00-12:00') The general approach was copied from PBS's traffic rule edit panel, but it is too different to generalize this into a new, reusable component. Signed-off-by: Lukas Wagner --- .../NotificationMatchExpressionEditPanel.js | 2 +- src/window/NotificationMatcherEdit.js | 300 +++++++++++++++++- 2 files changed, 290 insertions(+), 12 deletions(-) diff --git a/src/panel/NotificationMatchExpressionEditPanel.js b/src/panel/NotificationMatchExpressionEditPanel.js index 6d1f93e..e117ea5 100644 --- a/src/panel/NotificationMatchExpressionEditPanel.js +++ b/src/panel/NotificationMatchExpressionEditPanel.js @@ -87,7 +87,7 @@ Ext.define('Proxmox.panel.NotificationMatchExpressionEditPanel', { break; case 'match-calendar': data = { - value: '', + value: '00:00-23:59', }; leaf = true; break; diff --git a/src/window/NotificationMatcherEdit.js b/src/window/NotificationMatcherEdit.js index 893c3e3..433f86c 100644 --- a/src/window/NotificationMatcherEdit.js +++ b/src/window/NotificationMatcherEdit.js @@ -361,7 +361,7 @@ Ext.define('Proxmox.panel.NotificationRulesEditPanel', { break; case 'match-calendar': data = { - value: '', + value: '00:00-23:59', }; break; } @@ -977,6 +977,11 @@ Ext.define('Proxmox.panel.MatchCalendarSettings', { }, set: function (value) { let me = this; + + if (!me.get('typeIsMatchCalendar')) { + return; + } + let record = me.get('selectedRecord'); let currentData = record.get('data'); record.set({ @@ -992,23 +997,296 @@ Ext.define('Proxmox.panel.MatchCalendarSettings', { }, }, }, + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'grid checkbox': { + change: 'dowChanged', + }, + timefield: { + change: 'timeChanged', + }, + 'field[reference=timeframe]': { + change: 'setGridData', + }, + }, + + weekdays: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + + setGridData: function (field, value) { + let me = this; + + let record = me.parseTimeframe(value); + + me.lookup('weekdayGrid').getStore().setData([record]); + me.lookup('timeStart').setValue(record.start); + me.lookup('timeEnd').setValue(record.end); + }, + + parseTimeframe: function (timeframe) { + let me = this; + let [, days, start, end] = /^(?:(\S*)\s+)?([0-9:]+)-([0-9:]+)$/.exec(timeframe) || []; + + if (start === '0') { + start = '00:00'; + } + + let record = { + start, + end, + }; + + if (!days) { + days = 'mon..sun'; + } + + days = days.split(','); + days.forEach((day) => { + if (record[day]) { + return; + } + + if (me.weekdays.indexOf(day) !== -1) { + record[day] = true; + } else { + // we have a range 'xxx..yyy' + let [startDay, endDay] = day.split('..'); + let startIdx = me.weekdays.indexOf(startDay); + let endIdx = me.weekdays.indexOf(endDay); + + if (endIdx < startIdx) { + endIdx += me.weekdays.length; + } + + for (let dayIdx = startIdx; dayIdx <= endIdx; dayIdx++) { + let curDay = me.weekdays[dayIdx % me.weekdays.length]; + if (!record[curDay]) { + record[curDay] = true; + } + } + } + }); + + return record; + }, + + dowChanged: function (field, value) { + let me = this; + let record = field.getWidgetRecord(); + if (record === undefined) { + // this is sometimes called before a record/column is initialized + return; + } + let col = field.getWidgetColumn(); + record.set(col.dataIndex, value); + record.commit(); + + let startField = me.lookup('timeStart'); + let endField = me.lookup('timeEnd'); + + me.updateTimeframeField(startField, endField); + }, + + timeChanged: function (field, value) { + let me = this; + + let startField = me.lookup('timeStart'); + let endField = me.lookup('timeEnd'); + + let start = startField.getValue(); + let end = endField.getValue(); + + let valid = !(start && end && start >= end); + + if (!valid) { + startField.markInvalid(gettext('Start time must be before end time')); + endField.markInvalid(gettext('End time must be after start time')); + } else { + startField.clearInvalid(); + endField.clearInvalid(); + } + + me.updateTimeframeField(startField, endField); + }, + + updateTimeframeField: function (startField, endField) { + let me = this; + + let data = me.lookup('weekdayGrid').getStore().getData().getAt(0); + + let timeframe = me.formatSelectedDays(data.data); + + let start = me.formatTime(startField); + let end = me.formatTime(endField); + + timeframe += ` ${start}-${end}`; + + let field = me.lookup('timeframe'); + field.suspendEvent('change'); + field.setValue(timeframe); + + me.getViewModel().set('matchCalendarValue', timeframe); + + field.resumeEvent('change'); + }, + + formatSelectedDays: function (days) { + let me = this; + let selected = me.weekdays.filter((day) => days[day]); + + if (selected.length === 0) { + return ''; + } + if (selected.length === 1) { + return selected[0]; + } + + // Check if selected days are a contiguous block in weekday order + let indices = selected.map((day) => me.weekdays.indexOf(day)); + let isContiguous = indices.every((idx, i) => i === 0 || idx === indices[i - 1] + 1); + + if (isContiguous) { + return `${selected[0]}..${selected[selected.length - 1]}`; + } + + return selected.join(','); + }, + + formatTime: function (timefield) { + let value = timefield.getValue(); + + if (!value) { + return ''; + } + + let hours = value.getHours().toString().padStart(2, '0'); + let minutes = value.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}`; + }, + }, items: [ { - xtype: 'proxmoxKVComboBox', - fieldLabel: gettext('Timespan to match'), + xtype: 'hidden', + reference: 'timeframe', 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', ''], + }, + { + xtype: 'timefield', + reference: 'timeStart', + fieldLabel: gettext('Time Start'), + isFormField: false, + format: 'H:i', + formatText: 'HH:MM', + allowBlank: false, + }, + { + xtype: 'timefield', + reference: 'timeEnd', + fieldLabel: gettext('Time End'), + isFormField: false, + format: 'H:i', + formatText: 'HH:MM', + maxValue: '23:59', + allowBlank: false, + }, + { + xtype: 'fieldcontainer', + items: [ + { + xtype: 'grid', + margin: '10 0 0 0', + reference: 'weekdayGrid', + store: { + fields: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], + data: [ + { + mon: true, + tue: true, + wed: true, + thu: true, + fri: true, + sat: true, + sun: true, + }, + ], + }, + columns: [ + { + text: gettext('Mon'), + xtype: 'widgetcolumn', + dataIndex: 'mon', + flex: 1, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Tue'), + xtype: 'widgetcolumn', + dataIndex: 'tue', + flex: 1, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Wed'), + xtype: 'widgetcolumn', + dataIndex: 'wed', + flex: 1, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Thu'), + xtype: 'widgetcolumn', + dataIndex: 'thu', + flex: 1, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Fri'), + xtype: 'widgetcolumn', + dataIndex: 'fri', + flex: 1, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Sat'), + xtype: 'widgetcolumn', + dataIndex: 'sat', + flex: 1, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + { + text: gettext('Sun'), + xtype: 'widgetcolumn', + dataIndex: 'sun', + flex: 1, + widget: { + xtype: 'checkbox', + isFormField: false, + }, + }, + ], + }, ], }, ], -- 2.47.3