public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH manager widget-toolkit] Abstract /etc/hosts interface
@ 2023-05-31  9:59 Leo Nunner
  2023-05-31  9:59 ` [pve-devel] [PATCH manager] api: rework /etc/hosts API Leo Nunner
  2023-05-31  9:59 ` [pve-devel] [PATCH widget-toolkit] introduce abstractions for /etc/hosts view Leo Nunner
  0 siblings, 2 replies; 3+ messages in thread
From: Leo Nunner @ 2023-05-31  9:59 UTC (permalink / raw)
  To: pve-devel

This series introduces several endpoints for editing /etc/hosts via the
API. In accordance, the GUI has been adapted to move away from a simple
textarea towards a more complex interface. Details about the endpoints
can be found in the corresponding commit messages.

pve-manager:

Leo Nunner (1):
  api: rework /etc/hosts API

 PVE/API2/Nodes.pm | 298 +++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 283 insertions(+), 15 deletions(-)

pve-widget-toolkit:

Leo Nunner (1):
  introduce abstractions for /etc/hosts view

 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

-- 
2.30.2





^ permalink raw reply	[flat|nested] 3+ messages in thread

* [pve-devel] [PATCH manager] api: rework /etc/hosts API
  2023-05-31  9:59 [pve-devel] [PATCH manager widget-toolkit] Abstract /etc/hosts interface Leo Nunner
@ 2023-05-31  9:59 ` Leo Nunner
  2023-05-31  9:59 ` [pve-devel] [PATCH widget-toolkit] introduce abstractions for /etc/hosts view Leo Nunner
  1 sibling, 0 replies; 3+ messages in thread
From: Leo Nunner @ 2023-05-31  9:59 UTC (permalink / raw)
  To: pve-devel

Rework the API abstraction for /etc/nodes, which is located at
/nodes/{node} hosts. The endpoints are as follows:

    (1) GET /nodes/{node}/hosts
    (2) POST /nodes/{node}/hosts
    (3) GET /nodes/{node}/hosts/{line}
    (4) PUT /nodes/{node}/hosts/{line}
    (5) DELETE /nodes/{node}/hosts/{line}

Endpoint (1) provides a full list of all entries inside the /etc/hosts
file. They get split up into the following fields:
    - enabled
	Whether the line is commented out or not.
    - line
	The actual line number inside the file.
    - ip
	The IP address which is being mapped.
    - hosts
	The list of hostnames for the IP, as a comma-separated list.
    - value
	The raw line value as it is stored in /etc/hosts.

When "enabled" is set to false (0), the API will still try to parse the
value inside the comment. If it's an actual comment (and not just a
commented-out entry), "ip" and "hosts" will remain empty, while "value"
will contain the comment. There is no way to add new comments via the
API.

Endpoint (2) adds new entries to /etc/hosts. It takes a line number at
which the new entry should be inserted. If there are any entries *after*
this line, they get moved down by one position.

Endpoints (3), (4) and (5) are all for line based operations. (4) will
provide details about a specific line (with the same return values as in
(1), while (4) will update an existing line (with new 'enable', 'ip' and
'hosts' values). (5) will delete the specified line, and takes a 'move'
parameter which controlls whether the line should simply be replaced
with an empty line, or if it should be deleted (which causes all
proceeding lines to be moved up by one).

Signed-off-by: Leo Nunner <l.nunner@proxmox.com>
---
 PVE/API2/Nodes.pm | 298 +++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 283 insertions(+), 15 deletions(-)

diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index bfe5c40a1..d1e9aca8c 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -2208,24 +2208,71 @@ __PACKAGE__->register_method ({
 	},
     },
     returns => {
-	type => 'object',
-	properties => {
-	    digest => get_standard_option('pve-config-digest'),
-	    data => {
-		type => 'string',
-		description => 'The content of /etc/hosts.'
+	type => 'array',
+	items => {
+	    type => 'object',
+	    properties => {
+		enabled => {
+		    type => 'boolean',
+		    description => 'Enable or disable the entry.',
+		},
+		line => {
+		    type => 'integer',
+		    description => 'Line number of the entry.',
+		},
+		ip => {
+		    type => 'string',
+		    format => 'ip',
+		    description => 'Address to be mapped.',
+		    optional => 1,
+		},
+		hosts => {
+		    type => 'string',
+		    description => 'List of host names.',
+		    optional => 1,
+		},
+		value => {
+		    type => 'string',
+		    description => 'Raw line value.',
+		},
 	    },
 	},
     },
     code => sub {
 	my ($param) = @_;
 
-	return PVE::INotify::read_file('etchosts');
+	my $ret = [];
+	my $raw = PVE::INotify::read_file('etchosts');
+
+	my $pos = -1;
+	for my $line (split("\n", $raw->{data})) {
+	    $pos++;
+	    next if $line =~ m/^\s*$/; # whitespace/empty lines
 
+	    my $entry = {
+		line => $pos,
+		value => $line,
+		digest => $raw->{digest},
+	    };
+
+	    $entry->{enabled} = ($line !~ m/^\s*#/);
+
+	    my ($ip, @names) = split(/\s+/, $line);
+	    $ip =~ s/^#(.+)$/$1/;
+
+	    if (PVE::JSONSchema::pve_verify_ip($ip, 1)) {
+		$entry->{ip} = $ip;
+		$entry->{hosts} = join(',', @names);
+	    }
+
+	    push @$ret, $entry;
+	}
+
+	return $ret;
     }});
 
 __PACKAGE__->register_method ({
-    name => 'write_etc_hosts',
+    name => 'add_etc_hosts',
     path => 'hosts',
     method => 'POST',
     proxyto => 'node',
@@ -2233,15 +2280,170 @@ __PACKAGE__->register_method ({
     permissions => {
 	check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
     },
-    description => "Write /etc/hosts.",
+    description => "Update /etc/hosts.",
+    properties => {
+	enabled => {
+	    type => 'boolean',
+	    description => 'Enable or disable the entry.',
+	},
+	line => {
+	    type => 'integer',
+	    description => 'Line number of the entry.',
+	},
+	ip => {
+	    type => 'string',
+	    format => 'ip',
+	    description => 'Address to be mapped.',
+	    optional => 1,
+	},
+	hosts => {
+	    type => 'string',
+	    description => 'List of host names.',
+	    optional => 1,
+	},
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Tools::lock_file('/var/lock/pve-etchosts.lck', undef, sub {
+	    my $hosts = PVE::INotify::read_file('etchosts');
+
+	    my $out = "";
+	    my @lines = split("\n", $hosts->{data});
+	    my $pos = 0;
+	    while ($pos < scalar(@lines) || $pos <= $param->{line}) {
+		if ($pos == $param->{line}) {
+		    my $hosts_line = $param->{hosts};
+		    $hosts_line =~ s/,/ /g;
+		    $out .= "$param->{ip} $hosts_line \n";
+		}
+
+		$out .= $pos >= scalar(@lines) ? "\n" : "$lines[$pos]\n";
+		$pos++;
+	    }
+
+	    PVE::INotify::write_file('etchosts', $out);
+	});
+	die $@ if $@;
+
+	return;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'get_etc_hosts_line',
+    path => 'hosts/{line}',
+    method => 'GET',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/', [ 'Sys.Audit' ]],
+    },
+    description => "Get the content of /etc/hosts.",
     parameters => {
 	additionalProperties => 0,
 	properties => {
 	    node => get_standard_option('pve-node'),
+	    line => {
+		type => 'integer',
+		description => 'Line number.',
+	    },
+	},
+    },
+    returns => {
+	type => 'object',
+	properties => {
+	    enabled => {
+		type => 'boolean',
+		description => 'Enable or disable the entry.',
+	    },
+	    line => {
+		type => 'integer',
+		description => 'Line number of the entry.',
+	    },
+	    ip => {
+		type => 'string',
+		format => 'ip',
+		description => 'Address to be mapped.',
+		optional => 1,
+	    },
+	    hosts => {
+		type => 'string',
+		description => 'List of host names.',
+		optional => 1,
+	    },
+	    value => {
+		type => 'string',
+		description => 'Raw line value.',
+	    },
 	    digest => get_standard_option('pve-config-digest'),
-	    data => {
+	},
+    },
+    code => sub {
+	my ($param) = @_;
+
+	my $ret = undef;
+	my $raw = PVE::INotify::read_file('etchosts');
+
+	my $pos = -1;
+	for my $line (split("\n", $raw->{data})) {
+	    $pos++;
+	    next if $pos != $param->{line};
+
+	    $ret = {
+		line => $pos,
+		value => $line,
+		digest => $raw->{digest},
+	    };
+
+	    $ret->{enabled} = ($line !~ m/^\s*#/);
+
+	    my ($ip, @names) = split(/\s+/, $line);
+	    $ip =~ s/^#(.+)$/$1/;
+
+	    if (PVE::JSONSchema::pve_verify_ip($ip, 1)) {
+		$ret->{ip} = $ip;
+		$ret->{hosts} = join(',', @names);
+	    }
+	}
+
+	return $ret;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'update_etc_hosts',
+    path => 'hosts/{line}',
+    method => 'PUT',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
+    },
+    description => "Update /etc/hosts entry.",
+    parameters => {
+	type => 'object',
+	properties => {
+	    digest => get_standard_option('pve-config-digest'),
+	    enabled => {
+		type => 'boolean',
+		description => 'Entry is enabled',
+	    },
+	    line => {
+		type => 'integer',
+		description => 'Line number.',
+	    },
+	    ip => {
+		type => 'string',
+		format => 'ip',
+		description => 'Address.',
+		optional => 1,
+	    },
+	    hosts => {
 		type => 'string',
-		description =>  'The target content of /etc/hosts.'
+		description => 'List of host names.',
+		optional => 1,
 	    },
 	},
     },
@@ -2252,11 +2454,77 @@ __PACKAGE__->register_method ({
 	my ($param) = @_;
 
 	PVE::Tools::lock_file('/var/lock/pve-etchosts.lck', undef, sub {
-	    if ($param->{digest}) {
-		my $hosts = PVE::INotify::read_file('etchosts');
-		PVE::Tools::assert_if_modified($hosts->{digest}, $param->{digest});
+	    my $hosts = PVE::INotify::read_file('etchosts');
+	    PVE::Tools::assert_if_modified($hosts->{digest}, $param->{digest});
+
+	    my $out = "";
+	    my @lines = split("\n", $hosts->{data});
+	    my $pos = 0;
+	    while ($pos < scalar(@lines) || $pos <= $param->{line}) {
+		if ($pos == $param->{line}) {
+		    my $hosts_line = $param->{hosts};
+		    $hosts_line =~ s/,/ /g;
+		    $out .= ($param->{enabled} ? '' : '#') . "$param->{ip} $hosts_line \n";
+		} else {
+		    $out .= $pos >= scalar(@lines) ? "\n" : "$lines[$pos]\n";
+		}
+		$pos++;
 	    }
-	    PVE::INotify::write_file('etchosts', $param->{data});
+
+	    PVE::INotify::write_file('etchosts', $out);
+	});
+	die $@ if $@;
+
+	return;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'delete_etc_hosts',
+    path => 'hosts/{line}',
+    method => 'DELETE',
+    proxyto => 'node',
+    protected => 1,
+    permissions => {
+	check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
+    },
+    description => "Delete /etc/hosts entry.",
+    parameters => {
+	type => 'object',
+	properties => {
+	    line => {
+		type => 'integer',
+		description => 'Line number of the entry.',
+		optional => 0,
+	    },
+	    move => {
+		type => 'boolean',
+		description => 'Move up all following lines by 1.',
+		optional => 0,
+	    },
+	},
+    },
+    returns => {
+	type => 'null',
+    },
+    code => sub {
+	my ($param) = @_;
+
+	PVE::Tools::lock_file('/var/lock/pve-etchosts.lck', undef, sub {
+	    my $hosts = PVE::INotify::read_file('etchosts');
+
+	    my $out = "";
+	    my @lines = split("\n", $hosts->{data});
+	    my $pos = 0;
+	    while ($pos < scalar(@lines) || $pos <= $param->{line}) {
+		if ($pos == $param->{line}) {
+		    $out .= '\n' if !$param->{move};
+		} else	{
+		    $out .= $pos >= scalar(@lines) ? "\n" : "$lines[$pos]\n";
+		}
+		$pos++;
+	    }
+
+	    PVE::INotify::write_file('etchosts', $out);
 	});
 	die $@ if $@;
 
-- 
2.30.2





^ permalink raw reply	[flat|nested] 3+ messages in thread

* [pve-devel] [PATCH widget-toolkit] introduce abstractions for /etc/hosts view
  2023-05-31  9:59 [pve-devel] [PATCH manager widget-toolkit] Abstract /etc/hosts interface Leo Nunner
  2023-05-31  9:59 ` [pve-devel] [PATCH manager] api: rework /etc/hosts API Leo Nunner
@ 2023-05-31  9:59 ` Leo Nunner
  1 sibling, 0 replies; 3+ messages in thread
From: Leo Nunner @ 2023-05-31  9:59 UTC (permalink / raw)
  To: pve-devel

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@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





^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2023-05-31  9:59 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-05-31  9:59 [pve-devel] [PATCH manager widget-toolkit] Abstract /etc/hosts interface Leo Nunner
2023-05-31  9:59 ` [pve-devel] [PATCH manager] api: rework /etc/hosts API Leo Nunner
2023-05-31  9:59 ` [pve-devel] [PATCH widget-toolkit] introduce abstractions for /etc/hosts view Leo Nunner

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