From mboxrd@z Thu Jan  1 00:00:00 1970
Return-Path: <d.csapak@proxmox.com>
Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by lists.proxmox.com (Postfix) with ESMTPS id 80CEB9B401
 for <pve-devel@lists.proxmox.com>; Mon, 20 Nov 2023 14:45:16 +0100 (CET)
Received: from firstgate.proxmox.com (localhost [127.0.0.1])
 by firstgate.proxmox.com (Proxmox) with ESMTP id 62A691A28E
 for <pve-devel@lists.proxmox.com>; Mon, 20 Nov 2023 14:44:46 +0100 (CET)
Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com
 [94.136.29.106])
 (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
 key-exchange X25519 server-signature RSA-PSS (2048 bits))
 (No client certificate requested)
 by firstgate.proxmox.com (Proxmox) with ESMTPS
 for <pve-devel@lists.proxmox.com>; Mon, 20 Nov 2023 14:44:45 +0100 (CET)
Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1])
 by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 236FC4346E;
 Mon, 20 Nov 2023 14:44:45 +0100 (CET)
Message-ID: <862bb465-d873-4cb5-a57a-afe160ede751@proxmox.com>
Date: Mon, 20 Nov 2023 14:44:43 +0100
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird Beta
From: Dominik Csapak <d.csapak@proxmox.com>
To: Proxmox VE development discussion <pve-devel@lists.proxmox.com>,
 Stefan Hanreich <s.hanreich@proxmox.com>
References: <20231117114011.834002-1-s.hanreich@proxmox.com>
 <20231117114011.834002-21-s.hanreich@proxmox.com>
Content-Language: en-US
In-Reply-To: <20231117114011.834002-21-s.hanreich@proxmox.com>
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 7bit
X-SPAM-LEVEL: Spam detection results:  0
 AWL -1.483 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
 SPF_HELO_NONE           0.001 SPF: HELO does not publish an SPF Record
 SPF_PASS               -0.001 SPF: sender matches SPF record
 T_SCC_BODY_TEXT_LINE    -0.01 -
 URIBL_BLACK 3 Contains an URL listed in the URIBL blacklist [data.zone]
Subject: Re: [pve-devel] [PATCH v4 pve-manager 20/33] sdn: ipam: add ipam
 panel
X-BeenThere: pve-devel@lists.proxmox.com
X-Mailman-Version: 2.1.29
Precedence: list
List-Id: Proxmox VE development discussion <pve-devel.lists.proxmox.com>
List-Unsubscribe: <https://lists.proxmox.com/cgi-bin/mailman/options/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=unsubscribe>
List-Archive: <http://lists.proxmox.com/pipermail/pve-devel/>
List-Post: <mailto:pve-devel@lists.proxmox.com>
List-Help: <mailto:pve-devel-request@lists.proxmox.com?subject=help>
List-Subscribe: <https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel>, 
 <mailto:pve-devel-request@lists.proxmox.com?subject=subscribe>
X-List-Received-Date: Mon, 20 Nov 2023 13:45:16 -0000

generally looks ok, but i couldn't edit a manual dhcp
mapping with the error:

vmid: type check ('integer') failed - got ''

did not look too deep so i'm not sure where the empty vmid value
is coming from


also i like alexandres idea to put it into the main tree in the zone
panel as new panel that way we could even reduce the tree by one level
since we don't have to add the zone here

one minor thing inline (but that's not a blocker for me)

On 11/17/23 12:39, Stefan Hanreich wrote:
> Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
> ---
>   www/css/ext6-pve.css          |  22 ++-
>   www/manager6/Makefile         |   1 +
>   www/manager6/dc/Config.js     |  12 +-
>   www/manager6/sdn/IpamEdit.js  |  78 ++++++++++
>   www/manager6/tree/DhcpTree.js | 267 ++++++++++++++++++++++++++++++++++
>   5 files changed, 372 insertions(+), 8 deletions(-)
>   create mode 100644 www/manager6/sdn/IpamEdit.js
>   create mode 100644 www/manager6/tree/DhcpTree.js
> 
> diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
> index e18b173f5..091855356 100644
> --- a/www/css/ext6-pve.css
> +++ b/www/css/ext6-pve.css
> @@ -510,28 +510,38 @@ div.right-aligned {
>       content: ' ';
>   }
>   
> -.fa-sdn:before {
> +.x-fa-sdn-treelist:before {
>       width: 14px;
>       height: 14px;
>       position: absolute;
>       left: 1px;
>       top: 4px;
> +}
> +
> +.fa-sdn:before {
>       background-image:url(../images/icon-sdn.svg);
>       background-size: 14px 14px;
>       content: ' ';
>   }
>   
>   .fa-network-wired:before {
> -    width: 14px;
> -    height: 14px;
> -    position: absolute;
> -    left: 1px;
> -    top: 4px;
>       background-image:url(../images/icon-fa-network-wired.svg);
>       background-size: 14px 14px;
>       content: ' ';
>   }
>   
> +.x-fa-treepanel:before {
> +    width: 16px;
> +    height: 24px;
> +    display: block;
> +    background-repeat: no-repeat;
> +    background-position: center;
> +}
> +
> +.x-tree-icon-none {
> +    display: none;
> +}
> +
>   .x-treelist-row-over > * > .x-treelist-item-icon,
>   .x-treelist-row-over > * > .x-treelist-item-text{
>       color: #000;
> diff --git a/www/manager6/Makefile b/www/manager6/Makefile
> index 093452cd7..93b4ff155 100644
> --- a/www/manager6/Makefile
> +++ b/www/manager6/Makefile
> @@ -108,6 +108,7 @@ JSSRC= 							\
>   	tree/ResourceTree.js				\
>   	tree/SnapshotTree.js				\
>   	tree/ResourceMapTree.js				\
> +	tree/DhcpTree.js				\
>   	window/Backup.js				\
>   	window/BackupConfig.js				\
>   	window/BulkAction.js				\
> diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
> index 7d01da5fb..7c2b7b168 100644
> --- a/www/manager6/dc/Config.js
> +++ b/www/manager6/dc/Config.js
> @@ -185,7 +185,7 @@ Ext.define('PVE.dc.Config', {
>   		me.items.push({
>   		    xtype: 'pveSDNStatus',
>   		    title: gettext('SDN'),
> -		    iconCls: 'fa fa-sdn',
> +		    iconCls: 'fa fa-sdn x-fa-sdn-treelist',
>   		    hidden: true,
>   		    itemId: 'sdn',
>   		    expandedOnInit: true,
> @@ -203,7 +203,7 @@ Ext.define('PVE.dc.Config', {
>   		    groups: ['sdn'],
>   		    title: 'VNets',
>   		    hidden: true,
> -		    iconCls: 'fa fa-network-wired',
> +		    iconCls: 'fa fa-network-wired x-fa-sdn-treelist',
>   		    itemId: 'sdnvnet',
>   		},
>   		{
> @@ -213,6 +213,14 @@ Ext.define('PVE.dc.Config', {
>   		    hidden: true,
>   		    iconCls: 'fa fa-gear',
>   		    itemId: 'sdnoptions',
> +		},
> +		{
> +		    xtype: 'pveDhcpTree',
> +		    groups: ['sdn'],
> +		    title: gettext('IPAM'),
> +		    hidden: true,
> +		    iconCls: 'fa fa-map-signs',
> +		    itemId: 'sdnmappings',
>   		});
>   	    }
>   
> diff --git a/www/manager6/sdn/IpamEdit.js b/www/manager6/sdn/IpamEdit.js
> new file mode 100644
> index 000000000..18e22c592
> --- /dev/null
> +++ b/www/manager6/sdn/IpamEdit.js
> @@ -0,0 +1,78 @@
> +Ext.define('PVE.sdn.IpamEditInputPanel', {
> +    extend: 'Proxmox.panel.InputPanel',
> +    mixins: ['Proxmox.Mixin.CBind'],
> +
> +    isCreate: false,
> +
> +    onGetValues: function(values) {
> +	let me = this;
> +
> +	if (!values.vmid) {
> +	    delete values.vmid;
> +	}
> +
> +	return values;
> +    },
> +
> +    items: [
> +	{
> +	    xtype: 'pmxDisplayEditField',
> +	    name: 'vmid',
> +	    fieldLabel: gettext('VMID'),
> +	    allowBlank: false,
> +	    editable: false,
> +	    cbind: {
> +		hidden: '{isCreate}',
> +	    },
> +	},
> +	{
> +	    xtype: 'pmxDisplayEditField',
> +	    name: 'mac',
> +	    fieldLabel: gettext('MAC'),
> +	    allowBlank: false,
> +	    cbind: {
> +		editable: '{isCreate}',
> +	    },
> +	},
> +	{
> +	    xtype: 'proxmoxtextfield',
> +	    name: 'ip',
> +	    fieldLabel: gettext('IP'),
> +	    allowBlank: false,
> +	},
> +    ],
> +});
> +
> +Ext.define('PVE.sdn.IpamEdit', {
> +    extend: 'Proxmox.window.Edit',
> +
> +    subject: gettext('DHCP Mapping'),
> +    width: 350,
> +
> +    isCreate: false,
> +    mapping: {},
> +
> +    submitUrl: function(url, values) {
> +	return `${url}/${values.zone}/${values.vnet}/${values.mac}`;
> +    },
> +
> +    initComponent: function() {
> +	var me = this;
> +
> +	me.method = me.isCreate ? 'POST' : 'PUT';
> +
> +	let ipanel = Ext.create('PVE.sdn.IpamEditInputPanel', {
> +	    isCreate: me.isCreate,
> +	});
> +
> +	Ext.apply(me, {
> +	    items: [
> +		ipanel,
> +	    ],
> +	});
> +
> +	me.callParent();
> +
> +	ipanel.setValues(me.mapping);
> +    },
> +});
> diff --git a/www/manager6/tree/DhcpTree.js b/www/manager6/tree/DhcpTree.js
> new file mode 100644
> index 000000000..ca279c29a
> --- /dev/null
> +++ b/www/manager6/tree/DhcpTree.js
> @@ -0,0 +1,267 @@
> +Ext.define('PVE.sdn.DhcpTree', {
> +    extend: 'Ext.tree.Panel',
> +    xtype: 'pveDhcpTree',
> +
> +    layout: 'fit',
> +    rootVisible: false,
> +    animate: false,
> +
> +    store: {
> +	sorters: ['ip', 'name'],
> +    },
> +
> +    controller: {
> +	xclass: 'Ext.app.ViewController',
> +
> +	reload: function() {
> +	    let me = this;
> +
> +	    Proxmox.Utils.API2Request({
> +		url: `/cluster/sdn/ipam`,
> +		method: 'GET',
> +		success: function(response, opts) {
> +		    let root = {
> +			name: '__root',
> +			expanded: true,
> +			children: [],
> +		    };
> +
> +		    let zones = {};
> +		    let vnets = {};
> +		    let subnets = {};
> +
> +		    response.result.data.forEach((element) => {
> +			element.leaf = true;
> +
> +			if (!(element.zone in zones)) {
> +			    let zone = {
> +				name: element.zone,
> +				type: 'zone',
> +				iconCls: 'fa fa-th',
> +				expanded: true,
> +				children: [],
> +			    };
> +
> +			    zones[element.zone] = zone;
> +			    root.children.push(zone);
> +			}
> +
> +			if (!(element.vnet in vnets)) {
> +			    let vnet = {
> +				name: element.vnet,
> +				zone: element.zone,
> +				type: 'vnet',
> +				iconCls: 'fa fa-network-wired x-fa-treepanel',
> +				expanded: true,
> +				children: [],
> +			    };
> +
> +			    vnets[element.vnet] = vnet;
> +			    zones[element.zone].children.push(vnet);
> +			}
> +
> +			if (!(element.subnet in subnets)) {
> +			    let subnet = {
> +				name: element.subnet,
> +				zone: element.zone,
> +				vnet: element.vnet,
> +				type: 'subnet',
> +				iconCls: 'x-tree-icon-none',
> +				expanded: true,
> +				children: [],
> +			    };
> +
> +			    subnets[element.subnet] = subnet;
> +			    vnets[element.vnet].children.push(subnet);
> +			}
> +
> +			element.type = 'mapping';
> +			element.iconCls = 'x-tree-icon-none';
> +			subnets[element.subnet].children.push(element);
> +		    });
> +
> +		    me.getView().setRootNode(root);
> +		},
> +	    });
> +	},
> +
> +	init: function(view) {
> +	    let me = this;
> +	    me.reload();
> +	},
> +
> +	onDelete: function(table, rI, cI, item, e, { data }) {
> +	    let me = this;
> +	    let view = me.getView();
> +
> +	    Ext.Msg.show({
> +		title: gettext('Confirm'),
> +		icon: Ext.Msg.WARNING,
> +		message: Ext.String.format(gettext('Are you sure you want to remove DHCP mapping {0}'), `${data.mac} / ${data.ip}`),
> +		buttons: Ext.Msg.YESNO,
> +		defaultFocus: 'no',
> +		callback: function(btn) {
> +		    if (btn !== 'yes') {
> +		        return;
> +		    }
> +
> +		    Proxmox.Utils.API2Request({
> +			url: `/cluster/sdn/ipam/${data.zone}/${data.vnet}/${data.mac}`,
> +			method: 'DELETE',
> +			waitMsgTarget: view,
> +			failure: function(response, opts) {
> +			    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
> +			},
> +			callback: me.reload.bind(me),
> +		    });
> +		},
> +	    });
> +	},
> +
> +	editAction: function(_grid, _rI, _cI, _item, _e, rec) {
> +	    this.edit(rec);
> +	},
> +
> +	editDblClick: function() {
> +	    let me = this;
> +
> +	    let view = me.getView();
> +	    let selection = view.getSelection();
> +
> +	    if (!selection || selection.length < 1) {
> +		return;
> +	    }
> +
> +	    me.edit(selection[0]);
> +	},
> +
> +	edit: function(rec) {
> +	    let me = this;
> +
> +	    if (rec.data.type === 'mapping' && !rec.data.gateway) {
> +		me.openEditWindow(rec.data);
> +	    }
> +	},
> +
> +	openEditWindow: function(data) {
> +	    let me = this;
> +
> +	    Ext.create('PVE.sdn.IpamEdit', {
> +		autoShow: true,
> +		mapping: data,
> +		url: `/cluster/sdn/ipam`,
> +		extraRequestParams: {
> +		    vmid: data.vmid,
> +		    mac: data.mac,
> +		    zone: data.zone,
> +		    vnet: data.vnet,
> +		},
> +		listeners: {
> +		    destroy: () => me.reload(),
> +		},
> +	    });
> +	},
> +    },
> +
> +    listeners: {
> +	itemdblclick: 'editDblClick',
> +    },
> +
> +    tbar: [
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Reload'),
> +	    handler: 'reload',
> +	},
> +    ],
> +
> +    columns: [
> +	{
> +	    xtype: 'treecolumn',
> +	    text: gettext('Name / VMID'),
> +	    dataIndex: 'name',
> +	    width: 200,
> +	    renderer: function(value, meta, record) {
> +		if (record.get('gateway')) {
> +		    return gettext('Gateway');
> +		}
> +
> +		return record.get('name') ?? record.get('vmid') ?? ' ';
> +	    },
> +	},
> +	{
> +	    text: gettext('IP'),
> +	    dataIndex: 'ip',
> +	    width: 200,
> +	},
> +	{
> +	    text: gettext('MAC'),
> +	    dataIndex: 'mac',
> +	    width: 200,
> +	},
> +	{
> +	    text: gettext('Gateway'),
> +	    dataIndex: 'gateway',
> +	    width: 200,
> +	},
> +	{
> +	    header: gettext('Actions'),
> +	    xtype: 'actioncolumn',
> +	    dataIndex: 'text',
> +	    width: 150,
> +	    items: [
> +		{
> +		    handler: function(table, rI, cI, item, e, { data }) {
> +			let me = this;
> +
> +			Ext.create('PVE.sdn.IpamEdit', {
> +			    autoShow: true,
> +			    mapping: {},
> +			    url: `/cluster/sdn/ipam`,
> +			    isCreate: true,
> +			    extraRequestParams: {
> +				vnet: data.name,
> +				zone: data.zone,
> +			    },
> +			    listeners: {
> +				destroy: () => {
> +				    me.up('pveDhcpTree').controller.reload();
> +				},
> +			    },
> +			});
> +		    },

i'd put that function in the controller like the other handlers here

> +		    getTip: (v, m, rec) => gettext('Add'),
> +		    getClass: (v, m, { data }) => {
> +			if (data.type === 'vnet') {
> +			    return 'fa fa-plus-square';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +                },
> +		{
> +		    handler: 'editAction',
> +		    getTip: (v, m, rec) => gettext('Edit'),
> +		    getClass: (v, m, { data }) => {
> +			if (data.type === 'mapping' && !data.gateway) {
> +			    return 'fa fa-pencil fa-fw';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +                },
> +		{
> +		    handler: 'onDelete',
> +		    getTip: (v, m, rec) => gettext('Delete'),
> +		    getClass: (v, m, { data }) => {
> +			if (data.type === 'mapping' && !data.gateway) {
> +			    return 'fa critical fa-trash-o';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +                },
> +	    ],
> +	},
> +    ],
> +});