From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 3E2E81FF13A for ; Wed, 13 May 2026 11:30:09 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0149D5F5C; Wed, 13 May 2026 11:30:09 +0200 (CEST) From: Filip Schauer To: pve-devel@lists.proxmox.com Subject: [PATCH manager v4 3/3] ui: lxc/MPEdit: add "idmap" option Date: Wed, 13 May 2026 11:28:28 +0200 Message-ID: <20260513092830.47167-4-f.schauer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260513092830.47167-1-f.schauer@proxmox.com> References: <20260513092830.47167-1-f.schauer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1778664491093 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.038 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 PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: XPA7HG3CRYPN7IPCLOGBVYX4SAOPMD4X X-Message-ID-Hash: XPA7HG3CRYPN7IPCLOGBVYX4SAOPMD4X X-MailFrom: f.schauer@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Integrate UID/GID mapping for container mount points into the web UI. Signed-off-by: Filip Schauer --- Changes since v3: * Do not clear the list when toggling the passthrough checkbox. * Add a "Clear" button next to "Add". www/manager6/Makefile | 1 + www/manager6/lxc/IdMapField.js | 216 +++++++++++++++++++++++++++++++++ www/manager6/lxc/MPEdit.js | 8 ++ 3 files changed, 225 insertions(+) create mode 100644 www/manager6/lxc/IdMapField.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index f63437d6..85d973fe 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -210,6 +210,7 @@ JSSRC= \ lxc/DNS.js \ lxc/FeaturesEdit.js \ lxc/EnvEdit.js \ + lxc/IdMapField.js \ lxc/MPEdit.js \ lxc/MPResize.js \ lxc/Network.js \ diff --git a/www/manager6/lxc/IdMapField.js b/www/manager6/lxc/IdMapField.js new file mode 100644 index 00000000..81f1c764 --- /dev/null +++ b/www/manager6/lxc/IdMapField.js @@ -0,0 +1,216 @@ +Ext.define('PVE.lxc.IdMapField', { + extend: 'Ext.form.FieldContainer', + xtype: 'pveLxcIdMapField', + + layout: { type: 'vbox', align: 'stretch' }, + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'grid proxmoxintegerfield,grid proxmoxKVComboBox': { + change: function (widget, value) { + let me = this; + let record = widget.getWidgetRecord(); + let column = widget.getWidgetColumn(); + if (!record || !column) { + return; + } + record.set(column.dataIndex, value); + record.commit(); + me.updateIdMapField(); + }, + }, + }, + + onIdMapFieldChange: function (field, value) { + let me = this; + let passthrough = value === 'passthrough'; + let checkbox = me.lookup('passthrough'); + checkbox.suspendEvent('change'); + checkbox.setValue(passthrough); + checkbox.resumeEvent('change'); + me.lookup('idmaps').setVisible(!passthrough); + me.lookup('addIdMapButton').setVisible(!passthrough); + me.lookup('clearIdMapButton').setVisible(!passthrough); + + let store = me.lookup('idmaps').getStore(); + if (!passthrough && value) { + store.setData( + value.split(';').map((v) => { + let [type, ct, host, length] = v.split(':'); + return { type, ct, host, length }; + }), + ); + } else { + store.removeAll(); + } + }, + + onPassthroughCheckboxChange: function (checkbox, checked) { + let me = this; + let field = me.lookup('idmap'); + if (checked) { + me.stashedIdMap = field.getValue(); + field.setValue('passthrough'); + } else { + field.setValue(me.stashedIdMap || ''); + } + }, + + addIdMap: function () { + let me = this; + me.lookup('idmaps').getStore().add({ type: 'u', ct: '', host: '', length: '' }); + me.updateIdMapField(); + }, + + removeIdMap: function (button) { + let me = this; + me.lookup('idmaps').getStore().remove(button.getWidgetRecord()); + me.updateIdMapField(); + }, + + clearIdMap: function () { + let me = this; + me.lookup('idmaps').getStore().removeAll(); + me.updateIdMapField(); + }, + + updateIdMapField: function () { + let me = this; + let value = me + .lookup('idmaps') + .getStore() + .getRange() + .map(({ data: { type, ct, host, length } }) => `${type}:${ct}:${host}:${length}`) + .join(';'); + let field = me.lookup('idmap'); + field.suspendEvent('change'); + field.setValue(value); + field.resumeEvent('change'); + }, + }, + + items: [ + { + xtype: 'proxmoxcheckbox', + reference: 'passthrough', + fieldLabel: gettext('ID Mapping'), + boxLabel: gettext('Passthrough'), + isFormField: false, + listeners: { + change: 'onPassthroughCheckboxChange', + }, + }, + { + xtype: 'grid', + height: 170, + scrollable: true, + reference: 'idmaps', + viewConfig: { + emptyText: gettext('No ID maps configured'), + }, + store: { + fields: ['type', 'ct', 'host', 'length'], + data: [], + }, + columns: [ + { + text: gettext('ID Type'), + xtype: 'widgetcolumn', + dataIndex: 'type', + widget: { + xtype: 'proxmoxKVComboBox', + margin: '4 0', + allowBlank: false, + comboItems: [ + ['u', 'UID'], + ['g', 'GID'], + ], + }, + flex: 1, + }, + { + text: gettext('Container'), + xtype: 'widgetcolumn', + dataIndex: 'ct', + widget: { + xtype: 'proxmoxintegerfield', + margin: '4 0', + emptyText: gettext('Container'), + allowBlank: false, + minValue: 0, + }, + flex: 1, + }, + { + text: gettext('Host'), + xtype: 'widgetcolumn', + dataIndex: 'host', + widget: { + xtype: 'proxmoxintegerfield', + margin: '4 0', + emptyText: gettext('Host'), + allowBlank: false, + minValue: 0, + }, + flex: 1, + }, + { + text: gettext('Range Size'), + xtype: 'widgetcolumn', + dataIndex: 'length', + widget: { + xtype: 'proxmoxintegerfield', + margin: '4 0', + emptyText: gettext('Range Size'), + allowBlank: false, + minValue: 1, + }, + flex: 1, + }, + { + xtype: 'widgetcolumn', + width: 40, + widget: { + xtype: 'button', + margin: '4 0', + iconCls: 'fa fa-trash-o', + handler: 'removeIdMap', + }, + }, + ], + }, + { + xtype: 'container', + layout: { type: 'hbox' }, + defaults: { margin: '0 2' }, + items: [ + { + xtype: 'button', + reference: 'addIdMapButton', + text: gettext('Add'), + iconCls: 'fa fa-plus-circle', + handler: 'addIdMap', + flex: 1, + }, + { + xtype: 'button', + reference: 'clearIdMapButton', + text: gettext('Clear'), + iconCls: 'fa fa-trash-o', + handler: 'clearIdMap', + flex: 1, + }, + ], + }, + { + xtype: 'hidden', + reference: 'idmap', + name: 'idmap', + listeners: { + change: 'onIdMapFieldChange', + }, + }, + ], +}); diff --git a/www/manager6/lxc/MPEdit.js b/www/manager6/lxc/MPEdit.js index b1f67741..b193ff89 100644 --- a/www/manager6/lxc/MPEdit.js +++ b/www/manager6/lxc/MPEdit.js @@ -47,6 +47,7 @@ Ext.define('PVE.lxc.MountPointInputPanel', { setMPOpt('acl', values.acl); setMPOpt('replicate', values.replicate); setMPOpt('keepattrs', values.keepattrs); + setMPOpt('idmap', values.idmap); let res = {}; res[confid] = PVE.Parser.printLxcMountPoint(me.mp); @@ -353,6 +354,13 @@ Ext.define('PVE.lxc.MountPointInputPanel', { }, }, ], + + advancedColumnB: [ + { + xtype: 'pveLxcIdMapField', + name: 'idmap', + }, + ], }); Ext.define('PVE.lxc.MountPointEdit', { -- 2.47.3