public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH manager v2 5/5] ui: pci/usb mapping: rework mapping panel for better user experience
Date: Wed, 21 Jun 2023 09:41:42 +0200	[thread overview]
Message-ID: <20230621074142.742461-5-d.csapak@proxmox.com> (raw)
In-Reply-To: <20230621074142.742461-1-d.csapak@proxmox.com>

by removing the confusing buttons in the toolbar and adding them as
actions in an actioncolumn. There a only relevant actions are visible
and get a more expressive tooltip

with this, we now differentiate between 4 modes of the edit window:
* create a new mapping altogether
  - shows all fields
* edit existing mapping on top level
  - show only 'global' fields (comment, mdev), so no mappings
* add new host mapping
  - shows nodeselector, mapping (and mdev, but disabled)
    (informational only)
* edit existing host mapping
  - show selected node (displayfield) mdev and mappings, but only
    mappings are editable

we have to split the nodeselector into two fields, since the disabling
cbind does not pass through to the editconfig (and thus makes the form
invalid if we try that)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from rfc:
* for usb mappings too, so they are consistent
* moved the action column to second place
* hide/disable add button when there are mappings for every node
* use a bit different class to hide the buttons, because otherwise
  the grid cells would have the wrong height (since they would not
  get the styling of the font-awesome icon)
 www/css/ext6-pve.css                 |   5 +
 www/manager6/tree/ResourceMapTree.js | 178 +++++++++++++++++----------
 www/manager6/window/PCIMapEdit.js    |  40 ++++--
 www/manager6/window/USBMapEdit.js    |  49 ++++++--
 4 files changed, 183 insertions(+), 89 deletions(-)

diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
index 3af64255..edae462b 100644
--- a/www/css/ext6-pve.css
+++ b/www/css/ext6-pve.css
@@ -704,3 +704,8 @@ table.osds td:first-of-type {
 .x-grid-item .x-item-disabled {
     opacity: 0.3;
 }
+
+.pmx-action-hidden:before {
+    opacity: 0.0;
+    cursor: default;
+}
diff --git a/www/manager6/tree/ResourceMapTree.js b/www/manager6/tree/ResourceMapTree.js
index 02717042..4c476909 100644
--- a/www/manager6/tree/ResourceMapTree.js
+++ b/www/manager6/tree/ResourceMapTree.js
@@ -49,44 +49,89 @@ Ext.define('PVE.tree.ResourceMapTree', {
 	    });
 	},
 
-	addHost: function() {
+	add: function(_grid, _rI, _cI, _item, _e, rec) {
 	    let me = this;
-	    me.edit(false);
+	    if (rec.data.type !== 'entry') {
+		return;
+	    }
+
+	    me.openMapEditWindow(rec.data.name);
 	},
 
-	edit: function(includeNodename = true) {
+	editDblClick: function() {
 	    let me = this;
 	    let view = me.getView();
 	    let selection = view.getSelection();
-	    if (!selection || !selection.length) {
+	    if (!selection || selection.length < 1) {
 		return;
 	    }
-	    let rec = selection[0];
-	    if (!view.canConfigure || (rec.data.type === 'entry' && includeNodename)) {
+
+	    me.edit(selection[0]);
+	},
+
+	editAction: function(_grid, _rI, _cI, _item, _e, rec) {
+	    this.edit(rec);
+	},
+
+	edit: function(rec) {
+	    let me = this;
+	    if (rec.data.type === 'map') {
 		return;
 	    }
 
+	    me.openMapEditWindow(rec.data.name, rec.data.node, rec.data.type === 'entry');
+	},
+
+	openMapEditWindow: function(name, nodename, entryOnly) {
+	    let me = this;
+	    let view = me.getView();
+
 	    Ext.create(view.editWindowClass, {
-		url: `${view.baseUrl}/${rec.data.name}`,
+		url: `${view.baseUrl}/${name}`,
 		autoShow: true,
 		autoLoad: true,
-		nodename: includeNodename ? rec.data.node : undefined,
-		name: rec.data.name,
+		entryOnly,
+		nodename,
+		name,
 		listeners: {
 		    destroy: () => me.load(),
 		},
 	    });
 	},
 
-	remove: function() {
+	remove: function(_grid, _rI, _cI, _item, _e, rec) {
 	    let me = this;
+	    let msg, id;
 	    let view = me.getView();
-	    let selection = view.getSelection();
-	    if (!selection || !selection.length) {
-		return;
+	    let confirmMsg;
+	    switch (rec.data.type) {
+		case 'entry':
+		    msg = gettext("Are you sure you want to remove '{0}'");
+		    confirmMsg = Ext.String.format(msg, rec.data.name);
+		    break;
+		case 'node':
+		    msg = gettext("Are you sure you want to remove '{0}' entries for '{1}'");
+		    confirmMsg = Ext.String.format(msg, rec.data.node, rec.data.name);
+		    break;
+		case 'map':
+		    msg = gettext("Are you sure you want to remove '{0}' on '{1}' for '{2}'");
+		    id = rec.data[view.entryIdProperty];
+		    confirmMsg = Ext.String.format(msg, id, rec.data.node, rec.data.name);
+		    break;
+		default:
+		    throw "invalid type";
 	    }
+	    Ext.Msg.confirm(gettext('Confirm'), confirmMsg, function(btn) {
+		if (btn === 'yes') {
+		    me.executeRemove(rec.data);
+		}
+	    });
+	},
+
+	executeRemove: function(data) {
+	    let me = this;
+	    let view = me.getView();
 
-	    let data = selection[0].data;
 	    let url = `${view.baseUrl}/${data.name}`;
 	    let method = 'PUT';
 	    let params = {
@@ -233,6 +278,18 @@ Ext.define('PVE.tree.ResourceMapTree', {
 	    return `<i class="fa ${iconCls}"></i> ${status}`;
 	},
 
+	getAddClass: function(v, mD, rec) {
+	    let cls = 'fa fa-plus-circle';
+	    if (rec.data.type !== 'entry' || rec.data.children?.length >= PVE.data.ResourceStore.getNodes().length) {
+		cls += ' pmx-action-hidden';
+	    }
+	    return cls;
+	},
+
+	isAddDisabled: function(v, r, c, i, rec) {
+	    return rec.data.type !== 'entry' || rec.data.children?.length >= PVE.data.ResourceStore.getNodes().length;
+	},
+
 	init: function(view) {
 	    let me = this;
 
@@ -254,63 +311,56 @@ Ext.define('PVE.tree.ResourceMapTree', {
 
     tbar: [
 	{
-	    text: gettext('Add mapping'),
+	    text: gettext('Add'),
 	    handler: 'addMapping',
 	    cbind: {
 		disabled: '{!canConfigure}',
 	    },
 	},
-	{
-	    xtype: 'proxmoxButton',
-	    text: gettext('New Host mapping'),
-	    disabled: true,
-	    parentXType: 'treepanel',
-	    enableFn: function(_rec) {
-		return this.up('treepanel').canConfigure;
-	    },
-	    handler: 'addHost',
-	},
-	{
-	    xtype: 'proxmoxButton',
-	    text: gettext('Edit'),
-	    disabled: true,
-	    parentXType: 'treepanel',
-	    enableFn: function(rec) {
-		return rec && rec.data.type !== 'entry' && this.up('treepanel').canConfigure;
-	    },
-	    handler: 'edit',
-	},
-	{
-	    xtype: 'proxmoxButton',
-	    parentXType: 'treepanel',
-	    handler: 'remove',
-	    disabled: true,
-	    text: gettext('Remove'),
-	    enableFn: function(rec) {
-		return rec && this.up('treepanel').canConfigure;
-	    },
-	    confirmMsg: function(rec) {
-		let msg, id;
-		let view = this.up('treepanel');
-		switch (rec.data.type) {
-		    case 'entry':
-			msg = gettext("Are you sure you want to remove '{0}'");
-			return Ext.String.format(msg, rec.data.name);
-		    case 'node':
-			msg = gettext("Are you sure you want to remove '{0}' entries for '{1}'");
-			return Ext.String.format(msg, rec.data.node, rec.data.name);
-		    case 'map':
-			msg = gettext("Are you sure you want to remove '{0}' on '{1}' for '{2}'");
-			id = rec.data[view.entryIdProperty];
-			return Ext.String.format(msg, id, rec.data.node, rec.data.name);
-		    default:
-			throw "invalid type";
-		}
-	    },
-	},
     ],
 
     listeners: {
-	itemdblclick: 'edit',
+	itemdblclick: 'editDblClick',
+    },
+
+    initComponent: function() {
+	let me = this;
+
+	let columns = [...me.columns];
+	columns.splice(1, 0, {
+	    xtype: 'actioncolumn',
+	    text: gettext('Actions'),
+	    width: 80,
+	    items: [
+		{
+		    getTip: (v, m, { data }) =>
+			Ext.String.format(gettext("Add new host mapping for '{0}'"), data.name),
+		    getClass: 'getAddClass',
+		    isActionDisabled: 'isAddDisabled',
+		    handler: 'add',
+		},
+		{
+		    iconCls: 'fa fa-pencil',
+		    getTip: (v, m, { data }) => data.type === 'entry'
+			? Ext.String.format(gettext("Edit Mapping '{0}'"), data.name)
+			: Ext.String.format(gettext("Edit Mapping '{0}' for '{1}'"), data.name, data.node),
+		    getClass: (v, m, { data }) => data.type !== 'map' ? 'fa fa-pencil' : 'pmx-hidden',
+		    isActionDisabled: (v, r, c, i, rec) => rec.data.type === 'map',
+		    handler: 'editAction',
+		},
+		{
+		    iconCls: 'fa fa-trash-o',
+		    getTip: (v, m, { data }) => data.type === 'entry'
+			? Ext.String.format(gettext("Remove '{0}'"), data.name)
+			: data.type === 'node'
+			    ? Ext.String.format(gettext("Remove mapping for '{0}'"), data.node)
+			    : Ext.String.format(gettext("Remove mapping '{0}'"), data.path),
+		    handler: 'remove',
+		},
+	    ],
+	});
+	me.columns = columns;
+
+	me.callParent();
     },
 });
diff --git a/www/manager6/window/PCIMapEdit.js b/www/manager6/window/PCIMapEdit.js
index 2b268719..d43f04eb 100644
--- a/www/manager6/window/PCIMapEdit.js
+++ b/www/manager6/window/PCIMapEdit.js
@@ -13,8 +13,12 @@ Ext.define('PVE.window.PCIMapEditWindow', {
 
     cbindData: function(initialConfig) {
 	let me = this;
-	me.isCreate = !me.name || !me.nodename;
+	me.isCreate = (!me.name || !me.nodename) && !me.entryOnly;
 	me.method = me.name ? 'PUT' : 'POST';
+	me.hideMapping = !!me.entryOnly;
+	me.hideComment = me.name && !me.entryOnly;
+	me.hideNodeSelector = me.nodename || me.entryOnly;
+	me.hideNode = !me.nodename || !me.hideNodeSelector;
 	return {
 	    name: me.name,
 	    nodename: me.nodename,
@@ -201,35 +205,41 @@ Ext.define('PVE.window.PCIMapEditWindow', {
 		    allowBlank: false,
 		},
 		{
-		    xtype: 'pmxDisplayEditField',
+		    xtype: 'displayfield',
 		    fieldLabel: gettext('Mapping on Node'),
 		    labelWidth: 120,
 		    name: 'node',
-		    editConfig: {
-			xtype: 'pveNodeSelector',
-			reference: 'nodeselector',
-		    },
 		    cbind: {
-			editable: '{!nodename}',
 			value: '{nodename}',
+			disabled: '{hideNode}',
+			hidden: '{hideNode}',
+		    },
+		    allowBlank: false,
+		},
+		{
+		    xtype: 'pveNodeSelector',
+		    reference: 'nodeselector',
+		    fieldLabel: gettext('Mapping on Node'),
+		    labelWidth: 120,
+		    name: 'node',
+		    cbind: {
+			disabled: '{hideNodeSelector}',
+			hidden: '{hideNodeSelector}',
 		    },
 		    allowBlank: false,
 		},
 	    ],
 
 	    column2: [
-		{
-		    // as spacer
-		    xtype: 'displayfield',
-		},
 		{
 		    xtype: 'proxmoxcheckbox',
-		    fieldLabel: gettext('Mediated Devices'),
-		    labelWidth: 120,
+		    fieldLabel: gettext('Use with Mediated Devices'),
+		    labelWidth: 200,
 		    reference: 'mdev',
 		    name: 'mdev',
 		    cbind: {
 			deleteEmpty: '{!isCreate}',
+			disabled: '{hideComment}',
 		    },
 		},
 	    ],
@@ -244,6 +254,8 @@ Ext.define('PVE.window.PCIMapEditWindow', {
 		    name: 'map',
 		    cbind: {
 			nodename: '{nodename}',
+			disabled: '{hideMapping}',
+			hidden: '{hideMapping}',
 		    },
 		    allowBlank: false,
 		    onLoadCallBack: 'checkIommu',
@@ -257,6 +269,8 @@ Ext.define('PVE.window.PCIMapEditWindow', {
 		    name: 'description',
 		    cbind: {
 			deleteEmpty: '{!isCreate}',
+			disabled: '{hideComment}',
+			hidden: '{hideComment}',
 		    },
 		},
 	    ],
diff --git a/www/manager6/window/USBMapEdit.js b/www/manager6/window/USBMapEdit.js
index f36f1d03..358f0778 100644
--- a/www/manager6/window/USBMapEdit.js
+++ b/www/manager6/window/USBMapEdit.js
@@ -7,6 +7,10 @@ Ext.define('PVE.window.USBMapEditWindow', {
 	let me = this;
 	me.isCreate = !me.name;
 	me.method = me.isCreate ? 'POST' : 'PUT';
+	me.hideMapping = !!me.entryOnly;
+	me.hideComment = me.name && !me.entryOnly;
+	me.hideNodeSelector = me.nodename || me.entryOnly;
+	me.hideNode = !me.nodename || !me.hideNodeSelector;
 	return {
 	    name: me.name,
 	    nodename: me.nodename,
@@ -53,12 +57,14 @@ Ext.define('PVE.window.USBMapEditWindow', {
 	    if (me.originalMap) {
 		map = PVE.Parser.filterPropertyStringList(me.originalMap, (e) => e.node !== values.node);
 	    }
-	    map.push(PVE.Parser.printPropertyString(values));
+	    if (values.id) {
+		map.push(PVE.Parser.printPropertyString(values));
+	    }
 
-	    values = {
-		map,
-		description,
-	    };
+	    values = { map };
+	    if (description) {
+		values.description = description;
+	    }
 
 	    if (view.isCreate) {
 		values.id = name;
@@ -143,16 +149,26 @@ Ext.define('PVE.window.USBMapEditWindow', {
 		    allowBlank: false,
 		},
 		{
-		    xtype: 'pmxDisplayEditField',
-		    fieldLabel: gettext('Node'),
+		    xtype: 'displayfield',
+		    fieldLabel: gettext('Mapping on Node'),
+		    labelWidth: 120,
 		    name: 'node',
-		    editConfig: {
-			xtype: 'pveNodeSelector',
-			reference: 'nodeselector',
-		    },
 		    cbind: {
-			editable: '{!nodename}',
 			value: '{nodename}',
+			disabled: '{hideNode}',
+			hidden: '{hideNode}',
+		    },
+		    allowBlank: false,
+		},
+		{
+		    xtype: 'pveNodeSelector',
+		    reference: 'nodeselector',
+		    fieldLabel: gettext('Mapping on Node'),
+		    labelWidth: 120,
+		    name: 'node',
+		    cbind: {
+			disabled: '{hideNodeSelector}',
+			hidden: '{hideNodeSelector}',
 		    },
 		    allowBlank: false,
 		},
@@ -163,6 +179,10 @@ Ext.define('PVE.window.USBMapEditWindow', {
 		    xtype: 'fieldcontainer',
 		    defaultType: 'radiofield',
 		    layout: 'fit',
+		    cbind: {
+			disabled: '{hideMapping}',
+			hidden: '{hideMapping}',
+		    },
 		    items: [
 			{
 			    name: 'usb',
@@ -178,6 +198,7 @@ Ext.define('PVE.window.USBMapEditWindow', {
 			    name: 'id',
 			    cbind: {
 				nodename: '{nodename}',
+				disabled: '{hideMapping}',
 			    },
 			    editable: true,
 			    allowBlank: false,
@@ -214,6 +235,10 @@ Ext.define('PVE.window.USBMapEditWindow', {
 		    fieldLabel: gettext('Comment'),
 		    submitValue: true,
 		    name: 'description',
+		    cbind: {
+			disabled: '{hideComment}',
+			hidden: '{hideComment}',
+		    },
 		},
 	    ],
 	},
-- 
2.30.2





  parent reply	other threads:[~2023-06-21  7:41 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2023-06-21  7:41 [pve-devel] [PATCH manager v2 1/5] ui: resource map tree: make 'ok' status clearer Dominik Csapak
2023-06-21  7:41 ` [pve-devel] [PATCH manager v2 2/5] ui: pci map edit: reintroduce warnings checks Dominik Csapak
2023-06-21  7:41 ` [pve-devel] [PATCH manager v2 3/5] ui: pci/usb map edit: improve new host mappings dialog Dominik Csapak
2023-06-21  7:41 ` [pve-devel] [PATCH manager v2 4/5] ui: pci map edit: fix typos in warnings and use gettexts Dominik Csapak
2023-06-21  7:41 ` Dominik Csapak [this message]
2023-06-21  8:07 ` [pve-devel] applied: [PATCH manager v2 1/5] ui: resource map tree: make 'ok' status clearer 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=20230621074142.742461-5-d.csapak@proxmox.com \
    --to=d.csapak@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