[pve-devel] [PATCH widget-toolkit] introduce abstractions for /etc/hosts view

Leo Nunner l.nunner at proxmox.com
Wed May 31 11:59:19 CEST 2023


Remove the textarea and instead introduce a gridview. It shows all the
values that are being returned by the API endpoint:
    - Line
	The actual line number inside the file.
    - Enabled
	Whether the line is commented out or not.
    - IP
	The IP address which is being mapped.
    - Hosts
	The list of hostnames for the IP.
    - Value
	The raw line value as it is stored in /etc/hosts.

Entries can be added/edited/removed, and their 'Enabeld' status can be
toggled via the 'Toggle' button.

Signed-off-by: Leo Nunner <l.nunner at proxmox.com>
---
 src/Makefile                |   1 +
 src/node/HostsView.js       | 329 ++++++++++++++++++++++++++++--------
 src/panel/HostsEditPanel.js | 137 +++++++++++++++
 3 files changed, 401 insertions(+), 66 deletions(-)
 create mode 100644 src/panel/HostsEditPanel.js

diff --git a/src/Makefile b/src/Makefile
index 30e8fd5..63fe802 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -69,6 +69,7 @@ JSSRC=					\
 	panel/StatusView.js		\
 	panel/TfaView.js		\
 	panel/NotesView.js		\
+	panel/HostsEditPanel.js		\
 	window/Edit.js			\
 	window/PasswordEdit.js		\
 	window/SafeDestroy.js		\
diff --git a/src/node/HostsView.js b/src/node/HostsView.js
index 9adb6b2..8415d4a 100644
--- a/src/node/HostsView.js
+++ b/src/node/HostsView.js
@@ -1,66 +1,102 @@
+Ext.define('pve-etc-hosts-entry', {
+    extend: 'Ext.data.Model',
+    fields: [
+	{ name: 'enabled', type: 'boolean' },
+	{ name: 'ip', type: 'string' },
+	{ name: 'hosts', type: 'string' },
+	{ name: 'value', type: 'string' },
+	{ name: 'line', type: 'int' },
+    ],
+});
+
+Ext.define('Proxmox.node.HostsEditWindow', {
+    extend: 'Proxmox.window.Edit',
+
+    width: 600,
+
+    line: undefined,
+
+    initComponent: function() {
+	var me = this;
+
+	Ext.apply(me, {
+	    subject: "Hosts entry",
+	    defaultFocus: 'textfield[name=ip]',
+	});
+
+	Ext.apply(me, {
+	    items: [
+		{
+		    xtype: 'proxmoxcheckbox',
+		    name: 'enabled',
+		    dataIndex: 'enabled',
+		    fieldLabel: 'Enable',
+		    uncheckedValue: 0,
+		},
+		{
+		    xtype: me.isCreate ? 'numberfield' : 'hiddenfield',
+		    fieldLabel: 'Line',
+		    name: 'line',
+		    dataIndex: 'line',
+		    allowBlank: false,
+		},
+		{
+		    xtype: 'textfield',
+		    fieldLabel: gettext('IP'),
+		    name: 'ip',
+		    dataIndex: 'ip',
+		    allowBlank: false,
+		    vtype: 'IP64Address',
+		},
+		{
+		    xtype: 'fieldcontainer',
+		    fieldLabel: gettext('Hostnames'),
+		    items: [
+			{
+			    xtype: 'proxmoxHostsEditPanel', name: 'hosts',
+			},
+		    ],
+		},
+	    ],
+	});
+
+	let base_url = `/api2/extjs/nodes/${me.nodename}/hosts`;
+	if (me.isCreate) {
+            me.url = base_url;
+            me.method = 'POST';
+        } else {
+            me.url = base_url + `/${me.line}`;
+            me.method = 'PUT';
+        }
+
+	me.callParent();
+
+	let hostsPanel = me.down("proxmoxHostsEditPanel");
+	let hostsController = hostsPanel.getController();
+
+	me.setValues({ line: me.line });
+
+	if (!me.isCreate) { // do we already have data?
+	    me.load();
+	} else { // if not, we create a single empty host entry
+	    hostsController.addHost();
+	}
+    },
+});
+
 Ext.define('Proxmox.node.HostsView', {
-    extend: 'Ext.panel.Panel',
+    extend: 'Ext.grid.Panel',
     xtype: 'proxmoxNodeHostsView',
 
     reload: function() {
 	let me = this;
-	me.store.load();
+	let view = me.getView();
+	view.store.load();
     },
 
-    tbar: [
-	{
-	    text: gettext('Save'),
-	    disabled: true,
-	    itemId: 'savebtn',
-	    handler: function() {
-		let view = this.up('panel');
-		Proxmox.Utils.API2Request({
-		    params: {
-			digest: view.digest,
-			data: view.down('#hostsfield').getValue(),
-		    },
-		    method: 'POST',
-		    url: '/nodes/' + view.nodename + '/hosts',
-		    waitMsgTarget: view,
-		    success: function(response, opts) {
-			view.reload();
-		    },
-		    failure: function(response, opts) {
-			Ext.Msg.alert('Error', response.htmlStatus);
-		    },
-		});
-	    },
-	},
-	{
-	    text: gettext('Revert'),
-	    disabled: true,
-	    itemId: 'resetbtn',
-	    handler: function() {
-		let view = this.up('panel');
-		view.down('#hostsfield').reset();
-	    },
-	},
-    ],
+    layout: 'fit',
 
-	    layout: 'fit',
-
-    items: [
-	{
-	    xtype: 'textarea',
-	    itemId: 'hostsfield',
-	    fieldStyle: {
-		'font-family': 'monospace',
-		'white-space': 'pre',
-	    },
-	    listeners: {
-		dirtychange: function(ta, dirty) {
-		    let view = this.up('panel');
-		    view.down('#savebtn').setDisabled(!dirty);
-		    view.down('#resetbtn').setDisabled(!dirty);
-		},
-	    },
-	},
-    ],
+    nodename: undefined,
 
     initComponent: function() {
 	let me = this;
@@ -70,26 +106,187 @@ Ext.define('Proxmox.node.HostsView', {
 	}
 
 	me.store = Ext.create('Ext.data.Store', {
+	    model: 'pve-etc-hosts-entry',
 	    proxy: {
-		type: 'proxmox',
-		url: "/api2/json/nodes/" + me.nodename + "/hosts",
+                type: 'proxmox',
+                url: `/api2/json/nodes/${me.nodename}/hosts`,
 	    },
+	    sorters: [
+		{
+		    property: 'line',
+		    direction: 'ASC',
+		},
+	    ],
 	});
 
-	me.callParent();
+	var sm = Ext.create('Ext.selection.RowModel', {});
 
-	Proxmox.Utils.monStoreErrors(me, me.store);
-
-	me.mon(me.store, 'load', function(store, records, success) {
-	    if (!success || records.length < 1) {
+	var run_editor = function() {
+	    var rec = sm.getSelection()[0];
+	    if (!rec || !(rec.data.ip || rec.data.hosts)) {
 		return;
 	    }
-	    me.digest = records[0].data.digest;
-	    let data = records[0].data.data;
-	    me.down('#hostsfield').setValue(data);
-	    me.down('#hostsfield').resetOriginalValue();
+
+	    let win = Ext.create('Proxmox.node.HostsEditWindow', {
+		isCreate: false,
+		nodename: me.nodename,
+		line: rec.data.line,
+	    });
+	    win.on('destroy', me.reload, me);
+	    win.show();
+	};
+
+	Ext.apply(me, {
+	    tbar: [
+		{
+		    text: gettext('Add'),
+		    itemId: 'addbtn',
+		    handler: function() {
+			let items = me.store.getData().items;
+			let maxLine = items.reduce((a, v) => Math.max(a, v.data.line), -Infinity);
+			let win = Ext.create('Proxmox.node.HostsEditWindow', {
+			    isCreate: true,
+			    nodename: me.nodename,
+			    line: maxLine + 1,
+			});
+			win.on('destroy', me.reload, me);
+			win.show();
+		    },
+		},
+		{
+		    text: gettext('Edit'),
+		    itemId: 'editbtn',
+		    handler: run_editor,
+		},
+		{
+		    text: gettext('Delete'),
+		    itemId: 'deletebtn',
+		    handler: function() {
+			let rec = sm.getSelection()[0];
+
+			Proxmox.Utils.API2Request({
+			    method: 'DELETE',
+			    url: `/nodes/${me.nodename}/hosts/${rec.data.line}?move=1`,
+			    waitMsgTarget: me,
+			    success: function(response, opts) {
+				me.reload();
+			    },
+			    failure: function(response, opts) {
+				Ext.Msg.alert('Error', response.htmlStatus);
+			    },
+			});
+		    },
+		},
+		{
+		    text: gettext('Toggle'),
+		    itemId: 'togglebtn',
+		    handler: function() {
+			let rec = sm.getSelection()[0];
+			let params = rec.data;
+			params.enabled = params.enabled ? 0 : 1;
+
+			Proxmox.Utils.API2Request({
+			    method: 'PUT',
+			    url: `/nodes/${me.nodename}/hosts/${rec.data.line}`,
+			    params: params,
+			    waitMsgTarget: me,
+			    success: function(response, opts) {
+				me.reload();
+			    },
+			    failure: function(response, opts) {
+				Ext.Msg.alert('Error', response.htmlStatus);
+			    },
+			});
+		    },
+		},
+		'->',
+		gettext('Search') + ':',
+		' ',
+		{
+		    xtype: 'textfield',
+		    width: 200,
+		    enableKeyEvents: true,
+		    emptyText: gettext("IP, FQDN"),
+		    listeners: {
+			keyup: {
+			    buffer: 500,
+			    fn: function(field) {
+				let needle = field.getValue().toLocaleLowerCase();
+				me.store.clearFilter(true);
+				me.store.filter([
+				    {
+					filterFn: ({ data }) =>
+					data.ip?.toLocaleLowerCase().includes(needle) ||
+					data.hosts?.toLocaleLowerCase().includes(needle),
+				    },
+				]);
+			    },
+			},
+			change: function(field, newValue, oldValue) {
+			    if (newValue !== this.originalValue) {
+				this.triggers.clear.setVisible(true);
+			    }
+			},
+		    },
+		    triggers: {
+			clear: {
+			    cls: 'pmx-clear-trigger',
+			    weight: -1,
+			    hidden: true,
+			    handler: function() {
+				this.triggers.clear.setVisible(false);
+				this.setValue(this.originalValue);
+				me.store.clearFilter();
+			    },
+			},
+		    },
+		},
+	    ],
+	});
+
+	Ext.apply(me, {
+	    bodystyle: {
+		width: '100% !important',
+	    },
+	    columns: [
+		{
+		    text: gettext('Line'),
+		    dataIndex: 'line',
+		    width: 70,
+		},
+		{
+		    header: gettext('Enabled'),
+		    dataIndex: 'enabled',
+		    align: 'center',
+		    renderer: Proxmox.Utils.renderEnabledIcon,
+		    width: 90,
+		},
+		{
+		    text: gettext('IP'),
+		    dataIndex: 'ip',
+		    width: 150,
+		},
+		{
+		    text: gettext('Hosts'),
+		    dataIndex: 'hosts',
+		    flex: 1,
+		},
+		{
+		    text: gettext('Value'),
+		    dataIndex: 'value',
+		    flex: 1,
+		},
+	    ],
+	});
+
+	Ext.apply(me, {
+	    selModel: sm,
+	    listeners: {
+		itemdblclick: run_editor,
+	    },
 	});
 
+	me.callParent();
 	me.reload();
     },
 });
diff --git a/src/panel/HostsEditPanel.js b/src/panel/HostsEditPanel.js
new file mode 100644
index 0000000..34e99e1
--- /dev/null
+++ b/src/panel/HostsEditPanel.js
@@ -0,0 +1,137 @@
+Ext.define('Proxmox.node.HostsEntryEditor', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'proxmoxHostsEntryEditor',
+
+    layout: 'hbox',
+    border: false,
+
+    margin: '5 5 5 5',
+
+    initComponent: function() {
+	let me = this;
+
+	Ext.apply(me, {
+	    items: [
+		{
+		    xtype: 'proxmoxtextfield',
+		    width: 200,
+		    margin: '0 5 0 0 ',
+		    value: me.value,
+		    allowBlank: false,
+		    isFormField: false,
+		},
+		{
+		    xtype: 'button',
+		    iconCls: 'fa fa-trash-o',
+		    cls: 'removeLinkBtn',
+		    handler: function() {
+			let parent = this.up('proxmoxHostsEntryEditor');
+			if (parent.removeBtnHandler !== undefined) {
+			    parent.removeBtnHandler();
+			}
+		    },
+		},
+	    ],
+	});
+
+	me.callParent();
+    },
+});
+
+Ext.define('Proxmox.node.HostsEdit', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'proxmoxHostsEditPanel',
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	loadData: function(data) {
+	    let me = this;
+	    let hosts = data.split(",");
+
+	    let view = me.getView();
+	    view.query("proxmoxHostsEntryEditor").forEach((e) => view.remove(e.id));
+
+	    hosts.forEach((host) => {
+		me.addHost(host);
+	    });
+	},
+
+	addHost: function(value) {
+	    let me = this;
+	    let view = me.getView();
+	    let hostEdit = Ext.create('Proxmox.node.HostsEntryEditor', {
+		value: value,
+		removeBtnHandler: function() {
+		    view.remove(this);
+		    view.getController().setMarkerValue();
+		},
+	    });
+	    view.add(hostEdit);
+	},
+
+	addBtnHandler: function() {
+	    let me = this;
+	    me.addHost("");
+	    me.setMarkerValue();
+	},
+
+	setMarkerValue() {
+	    let me = this;
+	    let view = me.getView();
+	    view.inUpdate = true;
+	    me.lookup('marker').setValue(me.calculateValue());
+	    view.inUpdate = false;
+	},
+
+	calculateValue: function() {
+	    let me = this;
+	    let view = me.getView();
+
+	    let hosts = [];
+	    Ext.Array.each(view.query('proxmoxtextfield'), function(field) {
+		hosts.push(field.value);
+	    });
+	    return hosts.join(",");
+	},
+
+	control: {
+	    "proxmoxtextfield": {
+		change: "setMarkerValue",
+	    },
+	},
+    },
+
+    items: [
+	{
+	    xtype: 'hiddenfield',
+	    reference: 'marker',
+	    name: 'hosts',
+	    setValue: function(value) {
+		let me = this;
+		let panel = me.up('proxmoxHostsEditPanel');
+
+		if (!panel.inUpdate) {
+		    panel.getController().loadData(value);
+		}
+
+		return Ext.form.field.Hidden.prototype.setValue.call(this, value);
+	    },
+	},
+    ],
+
+    dockedItems: [{
+	xtype: 'toolbar',
+	dock: 'bottom',
+	defaultButtonUI: 'default',
+	border: false,
+	padding: '6 0 6 0',
+	items: [
+	    {
+		xtype: 'button',
+		text: gettext('Add'),
+		handler: 'addBtnHandler',
+	    },
+	],
+    }],
+});
-- 
2.30.2






More information about the pve-devel mailing list