From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 344951FF0AF for ; Fri, 06 Nov 2026 01:56:00 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id F08E821BA7; Fri, 06 Nov 2026 01:51:13 +0100 (CET) From: Thomas Lamprecht To: pve-devel@lists.proxmox.com Subject: [PATCH v3 manager 52/76] ui: storage wizard: preserve settings after partial setup Date: Fri, 6 Nov 2026 00:31:27 +0100 Message-ID: <20261105233336.2178064-53-t.lamprecht@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261105233336.2178064-1-t.lamprecht@proxmox.com> References: <20261105233336.2178064-1-t.lamprecht@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793926181036 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.691 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_MAILER 2 Automated Mailer Tag Left in Email RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust 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: FKAKLKW43ER3I2EKD5CPPTODELRR56B6 X-Message-ID-Hash: FKAKLKW43ER3I2EKD5CPPTODELRR56B6 X-MailFrom: t.lamprecht@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: Once setup creates its first object, keep settings fixed and disable Back so retries cannot mix unrelated storage IDs or devices. Record completed steps and retry only missing ones. Check storage IDs before starting to avoid partial setup on known conflicts. Allow types to validate node restrictions and explain setup state in confirmation notes. Signed-off-by: Thomas Lamprecht --- www/manager6/storage/wizard/CommonSettings.js | 20 +++-- www/manager6/storage/wizard/Wizard.js | 84 ++++++++++++++++++- 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/www/manager6/storage/wizard/CommonSettings.js b/www/manager6/storage/wizard/CommonSettings.js index 97e49e981..a2bd240fd 100644 --- a/www/manager6/storage/wizard/CommonSettings.js +++ b/www/manager6/storage/wizard/CommonSettings.js @@ -9,6 +9,7 @@ Ext.define('PVE.storage.wizard.CommonSettings', { // submitted as content instead of offering a selector fixedContent: undefined, contentFieldConfig: undefined, + nodesFieldConfig: undefined, onGetValues: function (values) { values.disable = values.enable ? 0 : 1; @@ -47,14 +48,17 @@ Ext.define('PVE.storage.wizard.CommonSettings', { } me.column2 = [ - { - xtype: 'pveNodeSelector', - name: 'nodes', - fieldLabel: gettext('Nodes'), - emptyText: gettext('All') + ' (' + gettext('No restrictions') + ')', - multiSelect: true, - autoSelect: false, - }, + Ext.apply( + { + xtype: 'pveNodeSelector', + name: 'nodes', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') + ')', + multiSelect: true, + autoSelect: false, + }, + me.nodesFieldConfig, + ), { xtype: 'proxmoxcheckbox', name: 'enable', diff --git a/www/manager6/storage/wizard/Wizard.js b/www/manager6/storage/wizard/Wizard.js index ce28f6845..ef3e505eb 100644 --- a/www/manager6/storage/wizard/Wizard.js +++ b/www/manager6/storage/wizard/Wizard.js @@ -23,6 +23,25 @@ PVE.storage.wizard.summaryFields = { options: { label: gettext('Mount Options') }, }; +PVE.storage.wizard.lvmNodesValidator = function () { + let wizard = this.up('window'); + if (wizard?.getViewModel().get('usage') !== 'lvm') { + return true; + } + let nodes = [] + .concat(this.getValue() ?? []) + .join(',') + .split(','); + nodes = nodes.filter((node) => node !== ''); + if (!nodes.length || nodes.includes(Proxmox.NodeName)) { + return true; + } + return Ext.String.format( + gettext('Must include node "{0}", which sets up the volume group.'), + Proxmox.NodeName, + ); +}; + Ext.define('PVE.storage.wizard.RemoteStorage', { extend: 'PVE.window.Wizard', alias: 'widget.pveRemoteStorageWizard', @@ -80,6 +99,9 @@ Ext.define('PVE.storage.wizard.RemoteStorage', { button.setDisabled(me.buttonStateBeforeBusy?.[idx] ?? false), ); delete me.buttonStateBeforeBusy; + if (me.frozenValues) { + me.down('#back').setDisabled(true); + } } }, @@ -108,6 +130,31 @@ Ext.define('PVE.storage.wizard.RemoteStorage', { }); }, + created: undefined, + + // once a first object exists, keep the settings it was created from: lock the other tabs and + // return the frozen values + freeze: function () { + let me = this; + if (me.frozenValues) { + return; + } + me.frozenValues = me.getValues(); + let tp = me.down('#wizcontent'); + let current = tp.getActiveTab(); + tp.items.each((tab) => { + if (tab !== current) { + tab.disable(); + } + }); + me.down('#back').setDisabled(true); + }, + + getValues: function (dirtyOnly) { + let me = this; + return me.frozenValues ? Ext.clone(me.frozenValues) : me.callParent([dirtyOnly]); + }, + // a step calls next() on success and just returns after a failure, so Finish retries from // there runSubmitChain: function (steps) { @@ -125,6 +172,41 @@ Ext.define('PVE.storage.wizard.RemoteStorage', { run(0); }, + // check all IDs up front, a conflict found halfway would leave objects behind; + // conflict(entries) returns safe HTML + assertStorageIdsFree: function (ids, next, conflict) { + let me = this; + me.request({ + url: '/storage', + method: 'GET', + failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), + success: function (response) { + let entries = response.result.data || []; + let taken = ids.filter((id) => entries.some((entry) => entry.storage === id)); + let msg = conflict?.(entries); + if (msg) { + Ext.Msg.alert(gettext('Error'), msg); + return; + } + if (taken.length) { + Ext.Msg.alert( + gettext('Error'), + Ext.String.format( + ngettext( + 'Storage ID {0} is already in use.', + 'Storage IDs {0} are already in use.', + taken.length, + ), + Ext.htmlEncode(taken.map((id) => `"${id}"`).join(', ')), + ), + ); + return; + } + next(); + }, + }); + }, + // errorHtml (safe HTML) is prepended to an error createStorage: function (params, next, errorHtml) { let me = this; @@ -232,7 +314,7 @@ Ext.define('PVE.storage.wizard.RemoteStorage', { let notes = panel.down('#summaryNotes'); notes.removeAll(); - for (const note of typeEntry.summaryNotes?.(values) || []) { + for (const note of typeEntry.summaryNotes?.(values, me) || []) { notes.add({ value: note }); } }, -- 2.47.3