public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pve-devel@lists.proxmox.com, pbs-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH widget-toolkit v3 07/14] notification: add UI for adding/updating webhook targets
Date: Fri,  8 Nov 2024 15:41:17 +0100	[thread overview]
Message-ID: <20241108144124.273550-8-l.wagner@proxmox.com> (raw)
In-Reply-To: <20241108144124.273550-1-l.wagner@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>
Tested-By: Stefan Hanreich <s.hanreich@proxmox.com>
---
 src/Makefile                  |   1 +
 src/Schema.js                 |   5 +
 src/Utils.js                  |  20 ++
 src/panel/WebhookEditPanel.js | 424 ++++++++++++++++++++++++++++++++++
 4 files changed, 450 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/Utils.js b/src/Utils.js
index 4ff95af..52375d2 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1524,6 +1524,26 @@ utilities: {
 	me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$");
 	me.Vlan_match = /^vlan(\d+)/;
 	me.VlanInterface_match = /(\w+)\.(\d+)/;
+
+
+	// Taken from proxmox-schema and ported to JS
+	let PORT_REGEX_STR = "(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])";
+	let IPRE_BRACKET_STR = "(?:" + IPV4_REGEXP + "|\\[(?:" + IPV6_REGEXP + ")\\])";
+	let DNS_NAME_STR = "(?:(?:" + DnsName_REGEXP + "\\.)*" + DnsName_REGEXP + ")";
+	let HTTP_URL_REGEX = "^https?://(?:(?:(?:"
+	    + DNS_NAME_STR
+	    + "|"
+	    + IPRE_BRACKET_STR
+	    + ")(?::"
+	    + PORT_REGEX_STR
+	    + ")?)|"
+	    + IPV6_REGEXP
+	    + ")(?:/[^\x00-\x1F\x7F]*)?$";
+
+	me.httpUrlRegex = new RegExp(HTTP_URL_REGEX);
+
+	// Same as SAFE_ID_REGEX in proxmox-schema
+	me.safeIdRegex = /^(?:[A-Za-z0-9_][A-Za-z0-9._\\-]*)$/;
     },
 });
 
diff --git a/src/panel/WebhookEditPanel.js b/src/panel/WebhookEditPanel.js
new file mode 100644
index 0000000..0a39f3c
--- /dev/null
+++ b/src/panel/WebhookEditPanel.js
@@ -0,0 +1,424 @@
+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'),
+	    regex: Proxmox.Utils.safeIdRegex,
+	    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',
+		    editable: false,
+		    value: 'post',
+		    comboItems: [
+			['post', 'POST'],
+			['put', 'PUT'],
+			['get', 'GET'],
+		    ],
+		    width: 80,
+		    margin: '0 5 0 0',
+		},
+		{
+		    xtype: 'proxmoxtextfield',
+		    name: 'url',
+		    allowBlank: false,
+		    emptyText: "https://example.com/hook",
+		    regex: Proxmox.Utils.httpUrlRegex,
+		    regexText: gettext('Must be a valid URL'),
+		    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 = Proxmox.Utils.base64ToUtf8(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 = Proxmox.Utils.utf8ToBase64(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);
+
+		// decode base64
+		let value = me.maskValues ? '' : Proxmox.Utils.base64ToUtf8(properties.value);
+
+		let obj = {
+		    headerName: properties.name,
+		    headerValue: value,
+		};
+
+		if (!me.isCreate && me.maskValues) {
+		    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: Proxmox.Utils.utf8ToBase64(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 (!rec.data.headerValue && !me.maskValues) {
+		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.5



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


  parent reply	other threads:[~2024-11-08 14:43 UTC|newest]

Thread overview: 19+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-11-08 14:41 [pve-devel] [PATCH many v3 00/14] notifications: add support for webhook endpoints Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox v3 01/14] notify: renderer: adapt to changes in proxmox-time Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox v3 02/14] notify: implement webhook targets Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox v3 03/14] notify: add api for " Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox-perl-rs v3 04/14] common: notify: add bindings for webhook API routes Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox-perl-rs v3 05/14] common: notify: add bindings for get_targets Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH widget-toolkit v3 06/14] utils: add base64 conversion helper Lukas Wagner
2024-11-10 17:27   ` [pve-devel] applied: " Thomas Lamprecht
2024-11-08 14:41 ` Lukas Wagner [this message]
2024-11-10 17:27   ` [pve-devel] applied: [PATCH widget-toolkit v3 07/14] notification: add UI for adding/updating webhook targets Thomas Lamprecht
2024-11-11 22:09   ` [pve-devel] " Thomas Lamprecht
2024-11-08 14:41 ` [pve-devel] [PATCH manager v3 08/14] api: notifications: use get_targets impl from proxmox-notify Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH manager v3 09/14] api: add routes for webhook notification endpoints Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH docs v3 10/14] notification: add documentation for webhook target endpoints Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox-backup v3 11/14] api: notification: add API routes for webhook targets Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox-backup v3 12/14] management cli: add CLI " Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox-backup v3 13/14] ui: utils: enable webhook edit window Lukas Wagner
2024-11-08 14:41 ` [pve-devel] [PATCH proxmox-backup v3 14/14] docs: notification: add webhook endpoint documentation Lukas Wagner
2024-11-11 22:02 ` [pve-devel] partially-applied: [PATCH many v3 00/14] notifications: add support for webhook endpoints Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20241108144124.273550-8-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal