From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <pbs-devel-bounces@lists.proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9])
	by lore.proxmox.com (Postfix) with ESMTPS id D01E31FF2D3
	for <inbox@lore.proxmox.com>; Fri, 12 Jul 2024 13:29:04 +0200 (CEST)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
	by firstgate.proxmox.com (Proxmox) with ESMTP id 991801EBA;
	Fri, 12 Jul 2024 13:28:47 +0200 (CEST)
From: Lukas Wagner <l.wagner@proxmox.com>
To: pve-devel@lists.proxmox.com,
	pbs-devel@lists.proxmox.com
Date: Fri, 12 Jul 2024 13:27:48 +0200
Message-Id: <20240712112755.123630-6-l.wagner@proxmox.com>
X-Mailer: git-send-email 2.39.2
In-Reply-To: <20240712112755.123630-1-l.wagner@proxmox.com>
References: <20240712112755.123630-1-l.wagner@proxmox.com>
MIME-Version: 1.0
X-SPAM-LEVEL: Spam detection results:  0
 AWL -0.143 Adjusted score from AWL reputation of From: address
 BAYES_00                 -1.9 Bayes spam probability is 0 to 1%
 DMARC_MISSING             0.1 Missing DMARC policy
 KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment
 POISEN_SPAM_PILL          0.1 Meta: its spam
 POISEN_SPAM_PILL_2        0.1 random spam to be learned in bayes
 POISEN_SPAM_PILL_4        0.1 random spam to be learned in bayes
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_PASS               -0.001 SPF: sender matches SPF record
Subject: [pbs-devel] [PATCH widget-toolkit v2 05/12] notification: add UI
 for adding/updating webhook targets
X-BeenThere: pbs-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox Backup Server development discussion
 <pbs-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pbs-devel>, 
 <mailto:pbs-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pbs-devel/>
List-Post: <mailto:pbs-devel@lists.proxmox.com>
List-Help: <mailto:pbs-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel>, 
 <mailto:pbs-devel-request@lists.proxmox.com?subject=subscribe>
Reply-To: Proxmox Backup Server development discussion
 <pbs-devel@lists.proxmox.com>
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Errors-To: pbs-devel-bounces@lists.proxmox.com
Sender: "pbs-devel" <pbs-devel-bounces@lists.proxmox.com>

The widgets for editing the headers/secrets were adapted from
the 'Tag Edit' dialog from PVE's datacenter options.

Apart from that, the new dialog is rather standard. I've decided
to put the http method and url in a single row, mostly to
save space and also to make it analogous to how an actual http request
is structured (VERB URL, followed by headers, followed by the body).

The secrets are a mechanism to store tokens/passwords in the
protected notification config. Secrets are accessible via
templating in the URL, headers and body via {{ secrets.NAME }}.
Secrets can only be set/updated, but not retrieved/displayed.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 src/Makefile                  |   1 +
 src/Schema.js                 |   5 +
 src/panel/WebhookEditPanel.js | 417 ++++++++++++++++++++++++++++++++++
 3 files changed, 423 insertions(+)
 create mode 100644 src/panel/WebhookEditPanel.js

diff --git a/src/Makefile b/src/Makefile
index 0478251..cfaffd7 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -78,6 +78,7 @@ JSSRC=					\
 	panel/StatusView.js		\
 	panel/TfaView.js		\
 	panel/NotesView.js		\
+	panel/WebhookEditPanel.js	\
 	window/Edit.js			\
 	window/PasswordEdit.js		\
 	window/SafeDestroy.js		\
diff --git a/src/Schema.js b/src/Schema.js
index 42541e0..cd1c306 100644
--- a/src/Schema.js
+++ b/src/Schema.js
@@ -65,6 +65,11 @@ Ext.define('Proxmox.Schema', { // a singleton
 	    ipanel: 'pmxGotifyEditPanel',
 	    iconCls: 'fa-bell-o',
 	},
+	webhook: {
+	    name: 'Webhook',
+	    ipanel: 'pmxWebhookEditPanel',
+	    iconCls: 'fa-bell-o',
+	},
     },
 
     // to add or change existing for product specific ones
diff --git a/src/panel/WebhookEditPanel.js b/src/panel/WebhookEditPanel.js
new file mode 100644
index 0000000..dfc7f3f
--- /dev/null
+++ b/src/panel/WebhookEditPanel.js
@@ -0,0 +1,417 @@
+Ext.define('Proxmox.panel.WebhookEditPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    xtype: 'pmxWebhookEditPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+    onlineHelp: 'notification_targets_webhook',
+
+    type: 'webhook',
+
+    columnT: [
+
+    ],
+
+    column1: [
+	{
+	    xtype: 'pmxDisplayEditField',
+	    name: 'name',
+	    cbind: {
+		value: '{name}',
+		editable: '{isCreate}',
+	    },
+	    fieldLabel: gettext('Endpoint Name'),
+	    allowBlank: false,
+	},
+    ],
+
+    column2: [
+	{
+	    xtype: 'proxmoxcheckbox',
+	    name: 'enable',
+	    fieldLabel: gettext('Enable'),
+	    allowBlank: false,
+	    checked: true,
+	},
+    ],
+
+    columnB: [
+	{
+	    layout: 'hbox',
+	    border: false,
+	    margin: '0 0 5 0',
+	    items: [
+		{
+		    xtype: 'displayfield',
+		    value: gettext('Method/URL:'),
+		    width: 125,
+		},
+		{
+		    xtype: 'proxmoxKVComboBox',
+		    name: 'method',
+		    //fieldLabel: gettext('Method'),
+		    editable: false,
+		    value: 'post',
+		    comboItems: [
+			['post', 'POST'],
+			['put', 'PUT'],
+			['get', 'GET'],
+		    ],
+		    width: 80,
+		    margin: '0 5 0 0',
+		},
+		{
+		    xtype: 'proxmoxtextfield',
+		    //fieldLabel: gettext('URL'),
+		    name: 'url',
+		    allowBlank: false,
+		    flex: 4,
+		},
+	    ],
+	},
+	{
+	    xtype: 'pmxWebhookKeyValueList',
+	    name: 'header',
+	    fieldLabel: gettext('Headers'),
+	    maskValues: false,
+	    cbind: {
+		isCreate: '{isCreate}',
+	    },
+	},
+	{
+	    xtype: 'textarea',
+	    fieldLabel: gettext('Body'),
+	    name: 'body',
+	    allowBlank: true,
+	    minHeight: '150',
+	    fieldStyle: {
+		'font-family': 'monospace',
+	    },
+	    margin: '15 0 0 0',
+	},
+	{
+	    xtype: 'pmxWebhookKeyValueList',
+	    name: 'secret',
+	    fieldLabel: gettext('Secrets'),
+	    maskValues: true,
+	    cbind: {
+		isCreate: '{isCreate}',
+	    },
+	},
+	{
+	    xtype: 'proxmoxtextfield',
+	    name: 'comment',
+	    fieldLabel: gettext('Comment'),
+	    cbind: {
+		deleteEmpty: '{!isCreate}',
+	    },
+	},
+    ],
+
+    onSetValues: (values) => {
+	values.enable = !values.disable;
+
+	if (values.body) {
+	    values.body = atob(values.body);
+	}
+
+	delete values.disable;
+	return values;
+    },
+
+    onGetValues: function(values) {
+	let me = this;
+
+	if (values.enable) {
+	    if (!me.isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
+	    }
+	} else {
+	    values.disable = 1;
+	}
+
+	if (values.body) {
+	    values.body = btoa(values.body);
+	} else {
+	    delete values.body;
+	    if (!me.isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'body' });
+	    }
+	}
+
+	if (Ext.isArray(values.header) && !values.header.length) {
+	    delete values.header;
+	    if (!me.isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'header' });
+	    }
+	}
+
+	if (Ext.isArray(values.secret) && !values.secret.length) {
+	    delete values.secret;
+	    if (!me.isCreate) {
+		Proxmox.Utils.assemble_field_data(values, { 'delete': 'secret' });
+	    }
+	}
+	delete values.enable;
+
+	return values;
+    },
+});
+
+Ext.define('Proxmox.form.WebhookKeyValueList', {
+    extend: 'Ext.container.Container',
+    alias: 'widget.pmxWebhookKeyValueList',
+
+    mixins: [
+	'Ext.form.field.Field',
+    ],
+
+    // override for column header
+    fieldTitle: gettext('Item'),
+
+    // will be applied to the textfields
+    maskRe: undefined,
+
+    allowBlank: true,
+    selectAll: false,
+    isFormField: true,
+    deleteEmpty: false,
+    config: {
+	deleteEmpty: false,
+	maskValues: false,
+    },
+
+    setValue: function(list) {
+	let me = this;
+
+	list = Ext.isArray(list) ? list : (list ?? '').split(';').filter(t => t !== '');
+
+	let store = me.lookup('grid').getStore();
+	if (list.length > 0) {
+	    store.setData(list.map(item => {
+		let properties = Proxmox.Utils.parsePropertyString(item);
+
+		let value = me.maskValues ? '' : atob(properties.value);
+
+		let obj = {
+		    headerName: properties.name,
+		    headerValue: value,
+		};
+
+		if (!me.isCreate) {
+		    obj.emptyText = gettext('Unchanged');
+		}
+
+		return obj;
+	    }));
+	} else {
+	    store.removeAll();
+	}
+	me.checkChange();
+	return me;
+    },
+
+    getValue: function() {
+	let me = this;
+	let values = [];
+	me.lookup('grid').getStore().each((rec) => {
+	    if (rec.data.headerName) {
+		let obj = {
+		    name: rec.data.headerName,
+		    value: btoa(rec.data.headerValue),
+		};
+
+		values.push(Proxmox.Utils.printPropertyString(obj));
+	    }
+	});
+
+	return values;
+    },
+
+    getErrors: function(value) {
+	let me = this;
+	let empty = false;
+
+	me.lookup('grid').getStore().each((rec) => {
+	    if (!rec.data.headerName) {
+		empty = true;
+	    }
+
+	    if (!rec.data.headerValue && rec.data.newValue) {
+		empty = true;
+	    }
+	});
+	if (empty) {
+	    return [gettext('Name/value must not be empty.')];
+	}
+	return [];
+    },
+
+    // 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.lookup('grid').getStore().add({
+		headerName: '',
+		headerValue: '',
+		emptyText: '',
+		newValue: true,
+	    });
+	},
+
+	removeSelection: function(field) {
+	    let me = this;
+	    let view = me.getView();
+	    let grid = me.lookup('grid');
+
+	    let record = field.getWidgetRecord();
+	    if (record === undefined) {
+		// this is sometimes called before a record/column is initialized
+		return;
+	    }
+
+	    grid.getStore().remove(record);
+	    view.checkChange();
+	    view.validate();
+	},
+
+	itemChange: function(field, newValue) {
+	    let rec = field.getWidgetRecord();
+	    if (!rec) {
+		return;
+	    }
+
+	    let column = field.getWidgetColumn();
+	    rec.set(column.dataIndex, newValue);
+	    let list = field.up('pmxWebhookKeyValueList');
+	    list.checkChange();
+	    list.validate();
+	},
+
+	control: {
+	    'grid button': {
+		click: 'removeSelection',
+	    },
+	},
+    },
+
+    margin: '10 0 5 0',
+
+    items: [
+	{
+	    layout: 'hbox',
+	    border: false,
+	    items: [
+		{
+		    xtype: 'displayfield',
+		    width: 125,
+		},
+		{
+		    xtype: 'button',
+		    text: gettext('Add'),
+		    iconCls: 'fa fa-plus-circle',
+		    handler: 'addLine',
+		    margin: '0 5 5 0',
+		},
+	    ],
+	},
+	{
+	    xtype: 'grid',
+	    reference: 'grid',
+	    minHeight: 100,
+	    maxHeight: 100,
+	    scrollable: 'vertical',
+	    margin: '0 0 0 125',
+
+	    viewConfig: {
+		deferEmptyText: false,
+	    },
+
+	    store: {
+		listeners: {
+		    update: function() {
+			this.commitChanges();
+		    },
+		},
+	    },
+	},
+    ],
+
+    initComponent: function() {
+	let me = this;
+
+	for (const [key, value] of Object.entries(me.gridConfig ?? {})) {
+	    me.items[1][key] = value;
+	}
+
+	me.items[0].items[0].value = me.fieldLabel + ':';
+
+	me.items[1].columns = [
+	    {
+		header: me.fieldTtitle,
+		dataIndex: 'headerName',
+		xtype: 'widgetcolumn',
+		widget: {
+		    xtype: 'textfield',
+		    isFormField: false,
+		    maskRe: me.maskRe,
+		    allowBlank: false,
+		    queryMode: 'local',
+		    listeners: {
+			change: 'itemChange',
+		    },
+		},
+		flex: 1,
+	    },
+	    {
+		header: me.fieldTtitle,
+		dataIndex: 'headerValue',
+		xtype: 'widgetcolumn',
+		widget: {
+		    xtype: 'proxmoxtextfield',
+		    inputType: me.maskValues ? 'password' : 'text',
+		    isFormField: false,
+		    maskRe: me.maskRe,
+		    queryMode: 'local',
+		    listeners: {
+			change: 'itemChange',
+		    },
+		    allowBlank: !me.isCreate && me.maskValues,
+
+		    bind: {
+			emptyText: '{record.emptyText}',
+		    },
+		},
+		flex: 1,
+	    },
+	    {
+		xtype: 'widgetcolumn',
+		width: 40,
+		widget: {
+		    xtype: 'button',
+		    iconCls: 'fa fa-trash-o',
+		},
+	    },
+	];
+
+	me.callParent();
+	me.initField();
+    },
+});
-- 
2.39.2



_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel