From: Elias Huhsovitz <e.huhsovitz@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Elias Huhsovitz <e.huhsovitz@proxmox.com>
Subject: [PATCH manager v3 1/2] fix #6855: ui: storage: add mount options field for nfs amd smb/cifs
Date: Thu, 30 Jul 2026 11:40:54 +0200 [thread overview]
Message-ID: <20260730094055.57659-2-e.huhsovitz@proxmox.com> (raw)
In-Reply-To: <20260730094055.57659-1-e.huhsovitz@proxmox.com>
Add a text field for custom mount options to the NFS and CIFS
storage configuration dialogs. This allows users to configure
advanced mount parameters (like `nconnect`, `rdma`, or `seal`)
directly via the web interface, without needing to use the CLI
or manually edit storage.cfg.
When nfs version or mount option is being changed:
Display a hint to the user that changes will require
remounting the storage to take effect.
Reduce the usage of legacy JS practices, such as `var` & `+` string
concatination in affected functions.
Remove unused class property `options`.
Signed-off-by: Elias Huhsovitz <e.huhsovitz@proxmox.com>
---
www/manager6/storage/CIFSEdit.js | 69 +++++++++++++++++--
www/manager6/storage/NFSEdit.js | 112 ++++++++++++++++++++++++-------
2 files changed, 152 insertions(+), 29 deletions(-)
diff --git a/www/manager6/storage/CIFSEdit.js b/www/manager6/storage/CIFSEdit.js
index 5fe3fefe..47e2118a 100644
--- a/www/manager6/storage/CIFSEdit.js
+++ b/www/manager6/storage/CIFSEdit.js
@@ -121,7 +121,7 @@ Ext.define('PVE.storage.CIFSInputPanel', {
onlineHelp: 'storage_cifs',
onGetValues: function (values) {
- let me = this;
+ const me = this;
if (values.password?.length === 0) {
delete values.password;
@@ -133,11 +133,39 @@ Ext.define('PVE.storage.CIFSInputPanel', {
delete values.subdir;
}
+ if (values.options) {
+ values.options = values.options
+ .split(',')
+ .map((opt) => opt.trim())
+ .filter((opt, index, self) => opt !== '' && self.indexOf(opt) === index)
+ .join(',');
+ }
+
+ if (!values.options) {
+ delete values.options;
+ if (!me.isCreate) {
+ values.delete = values.delete ? `${values.delete},options` : 'options';
+ }
+ }
+
return me.callParent([values]);
},
+ setValues: function (values) {
+ const me = this;
+
+ me.originalOptions = values.options || '';
+
+ me.callParent([values]);
+
+ const hint = me.down('#mountOptionsHint');
+ if (hint) {
+ hint.setHidden(true);
+ }
+ },
+
initComponent: function () {
- var me = this;
+ const me = this;
me.column1 = [
{
@@ -149,7 +177,7 @@ Ext.define('PVE.storage.CIFSInputPanel', {
listeners: {
change: function (f, value) {
if (me.isCreate) {
- let exportField = me.down('field[name=share]');
+ const exportField = me.down('field[name=share]');
exportField.setServer(value);
}
},
@@ -166,7 +194,7 @@ Ext.define('PVE.storage.CIFSInputPanel', {
if (!me.isCreate) {
return;
}
- var exportField = me.down('field[name=share]');
+ const exportField = me.down('field[name=share]');
exportField.setUsername(value);
},
},
@@ -181,7 +209,7 @@ Ext.define('PVE.storage.CIFSInputPanel', {
minLength: 1,
listeners: {
change: function (f, value) {
- let exportField = me.down('field[name=share]');
+ const exportField = me.down('field[name=share]');
exportField.setPassword(value);
},
},
@@ -213,7 +241,7 @@ Ext.define('PVE.storage.CIFSInputPanel', {
listeners: {
change: function (f, value) {
if (me.isCreate) {
- let exportField = me.down('field[name=share]');
+ const exportField = me.down('field[name=share]');
exportField.setDomain(value);
}
},
@@ -229,6 +257,35 @@ Ext.define('PVE.storage.CIFSInputPanel', {
},
];
+ me.advancedColumn2 = [
+ {
+ xtype: 'textfield',
+ name: 'options',
+ fieldLabel: gettext('Mount Options'),
+ emptyText: `${gettext('Example')}: rdma, seal`,
+ allowBlank: true,
+ listeners: {
+ change: function (field, newValue) {
+ const hint = me.down('#mountOptionsHint');
+ if (hint) {
+ hint.setHidden(me.isCreate || (newValue || '') === me.originalOptions);
+ }
+ },
+ },
+ },
+ ];
+
+ me.advancedColumnB = me.advancedColumnB || [];
+ me.advancedColumnB.unshift({
+ xtype: 'displayfield',
+ itemId: 'mountOptionsHint',
+ userCls: 'pmx-hint',
+ hidden: true,
+ value: gettext(
+ 'Changes to mount options require remounting the storage to take effect.',
+ ),
+ });
+
me.callParent();
},
});
diff --git a/www/manager6/storage/NFSEdit.js b/www/manager6/storage/NFSEdit.js
index 72db156f..68f4a8ce 100644
--- a/www/manager6/storage/NFSEdit.js
+++ b/www/manager6/storage/NFSEdit.js
@@ -57,34 +57,37 @@ Ext.define('PVE.storage.NFSScan', {
me.callParent();
},
});
-
Ext.define('PVE.storage.NFSInputPanel', {
extend: 'PVE.panel.StorageBase',
onlineHelp: 'storage_nfs',
- options: [],
-
onGetValues: function (values) {
- var me = this;
-
- var i;
- var res = [];
- for (i = 0; i < me.options.length; i++) {
- let item = me.options[i];
- if (!item.match(/^vers=(.*)$/)) {
- res.push(item);
- }
+ const me = this;
+ const res = [];
+
+ if (values.options && values.options.trim() !== '') {
+ const opts = values.options.split(',');
+ opts.forEach((opt) => {
+ const cleanOpt = opt.trim();
+ if (cleanOpt !== '' && !cleanOpt.match(/^vers=(.*)$/) && !res.includes(cleanOpt)) {
+ res.push(cleanOpt);
+ }
+ });
}
+
if (values.nfsversion && values.nfsversion !== '__default__') {
- res.push('vers=' + values.nfsversion);
+ res.push(`vers=${values.nfsversion}`);
}
+
delete values.nfsversion;
- values.options = res.join(',');
- if (values.options === '') {
+
+ if (res.length > 0) {
+ values.options = res.join(',');
+ } else {
delete values.options;
if (!me.isCreate) {
- values.delete = 'options';
+ values.delete = values.delete ? `${values.delete},options` : 'options';
}
}
@@ -92,21 +95,42 @@ Ext.define('PVE.storage.NFSInputPanel', {
},
setValues: function (values) {
- var me = this;
+ const me = this;
+
if (values.options) {
- me.options = values.options.split(',');
- me.options.forEach(function (item) {
- var match = item.match(/^vers=(.*)$/);
+ const opts = values.options.split(',').map((item) => item.trim());
+
+ opts.forEach((item) => {
+ const match = item.match(/^vers=(.*)$/);
if (match) {
values.nfsversion = match[1];
}
});
+
+ // Filter out "vers=..." and empty strings
+ const otherOptions = opts.filter(
+ (item, index, self) => item !== '' && !item.match(/^vers=(.*)$/),
+ );
+ values.options = otherOptions.join(',');
+
+ if (values.options === '') {
+ delete values.options;
+ }
+ }
+
+ me.originalOptions = values.options || '';
+ me.originalNfsVersion = values.nfsversion || '__default__';
+
+ me.callParent([values]);
+
+ const hint = me.down('#mountOptionsHint');
+ if (hint) {
+ hint.setHidden(true);
}
- return me.callParent([values]);
},
initComponent: function () {
- var me = this;
+ const me = this;
me.column1 = [
{
@@ -118,7 +142,7 @@ Ext.define('PVE.storage.NFSInputPanel', {
listeners: {
change: function (f, value) {
if (me.isCreate) {
- let exportField = me.down('field[name=export]');
+ const exportField = me.down('field[name=export]');
exportField.setServer(value);
exportField.setValue('');
}
@@ -143,6 +167,25 @@ Ext.define('PVE.storage.NFSInputPanel', {
];
me.advancedColumn2 = [
+ {
+ xtype: 'textfield',
+ name: 'options',
+ fieldLabel: gettext('Mount Options'),
+ emptyText: `${gettext('Example')}: nconnect=4, soft`,
+ allowBlank: true,
+ listeners: {
+ change: function (field, newValue) {
+ const hint = me.down('#mountOptionsHint');
+ if (hint) {
+ const currentVersion =
+ me.down('field[name=nfsversion]')?.getValue() || '__default__';
+ const isOptionsChanged = (newValue || '') !== me.originalOptions;
+ const isVersionChanged = currentVersion !== me.originalNfsVersion;
+ hint.setHidden(me.isCreate || !(isOptionsChanged || isVersionChanged));
+ }
+ },
+ },
+ },
{
xtype: 'proxmoxKVComboBox',
fieldLabel: gettext('NFS Version'),
@@ -156,9 +199,32 @@ Ext.define('PVE.storage.NFSInputPanel', {
['4.1', '4.1'],
['4.2', '4.2'],
],
+ listeners: {
+ change: function (field, newValue) {
+ const hint = me.down('#mountOptionsHint');
+ if (hint) {
+ const currentOptions = me.down('field[name=options]')?.getValue() || '';
+ const isOptionsChanged = currentOptions !== me.originalOptions;
+ const isVersionChanged =
+ (newValue || '__default__') !== me.originalNfsVersion;
+ hint.setHidden(me.isCreate || !(isOptionsChanged || isVersionChanged));
+ }
+ },
+ },
},
];
+ me.advancedColumnB = me.advancedColumnB || [];
+ me.advancedColumnB.unshift({
+ xtype: 'displayfield',
+ itemId: 'mountOptionsHint',
+ userCls: 'pmx-hint',
+ hidden: true,
+ value: gettext(
+ 'Changes to mount options require remounting the storage to take effect.',
+ ),
+ });
+
me.callParent();
},
});
--
2.47.3
next prev parent reply other threads:[~2026-07-30 9:41 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-30 9:40 [PATCH manager v3 0/2] fix #6855: ui: storage: add mount options field for nfs, cifs, cephfs Elias Huhsovitz
2026-07-30 9:40 ` Elias Huhsovitz [this message]
2026-07-30 9:40 ` [PATCH manager v3 2/2] ui: storage: add mount options field for cephfs Elias Huhsovitz
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=20260730094055.57659-2-e.huhsovitz@proxmox.com \
--to=e.huhsovitz@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