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 DE3A51FF13C for ; Thu, 19 Mar 2026 17:13:54 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 160F54E0F; Thu, 19 Mar 2026 17:14:10 +0100 (CET) From: Hannes Laimer To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup v5 8/9] ui: add move namespace action Date: Thu, 19 Mar 2026 17:13:24 +0100 Message-ID: <20260319161325.206846-9-h.laimer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260319161325.206846-1-h.laimer@proxmox.com> References: <20260319161325.206846-1-h.laimer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1773936780760 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.082 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 Message-ID-Hash: NUOOITDRKCWLJN5IPE47CTEMOJXBQV27 X-Message-ID-Hash: NUOOITDRKCWLJN5IPE47CTEMOJXBQV27 X-MailFrom: h.laimer@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 Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: When moving a namespace, exclude the source namespace and its subtree from the 'New Parent' selector. Signed-off-by: Hannes Laimer --- www/Makefile | 1 + www/datastore/Content.js | 27 +++++++++++- www/form/NamespaceSelector.js | 11 +++++ www/window/NamespaceMove.js | 79 +++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 www/window/NamespaceMove.js diff --git a/www/Makefile b/www/Makefile index 7745d9f4..f2011af3 100644 --- a/www/Makefile +++ b/www/Makefile @@ -80,6 +80,7 @@ JSSRC= \ window/CreateDirectory.js \ window/DataStoreEdit.js \ window/NamespaceEdit.js \ + window/NamespaceMove.js \ window/MaintenanceOptions.js \ window/NotesEdit.js \ window/RemoteEdit.js \ diff --git a/www/datastore/Content.js b/www/datastore/Content.js index b56cc4ac..e3e0d0c9 100644 --- a/www/datastore/Content.js +++ b/www/datastore/Content.js @@ -667,6 +667,26 @@ Ext.define('PBS.DataStoreContent', { }); }, + moveNS: function () { + let me = this; + let view = me.getView(); + if (!view.namespace || view.namespace === '') { + return; + } + let win = Ext.create('PBS.window.NamespaceMove', { + datastore: view.datastore, + namespace: view.namespace, + taskDone: (success) => { + if (success) { + let newNs = win.getNewNamespace(); + view.down('pbsNamespaceSelector').store?.load(); + me.nsChange(null, newNs); + } + }, + }); + win.show(); + }, + moveGroup: function (data) { let me = this; let view = me.getView(); @@ -685,6 +705,8 @@ Ext.define('PBS.DataStoreContent', { let me = this; if (data.ty === 'group') { me.moveGroup(data); + } else if (data.ty === 'ns') { + me.moveNS(); } }, @@ -1113,10 +1135,13 @@ Ext.define('PBS.DataStoreContent', { if (data.ty === 'group') { return Ext.String.format(gettext("Move group '{0}'"), v); } - return ''; + return Ext.String.format(gettext("Move namespace '{0}'"), v); }, getClass: (v, m, { data }) => { if (data.ty === 'group') { return 'fa fa-arrows'; } + if (data.ty === 'ns' && !data.isRootNS && data.ns === undefined) { + return 'fa fa-arrows'; + } return 'pmx-hidden'; }, isActionDisabled: (v, r, c, i, { data }) => false, diff --git a/www/form/NamespaceSelector.js b/www/form/NamespaceSelector.js index ddf68254..d349b568 100644 --- a/www/form/NamespaceSelector.js +++ b/www/form/NamespaceSelector.js @@ -90,6 +90,17 @@ Ext.define('PBS.form.NamespaceSelector', { }, }); + if (me.excludeNs) { + me.store.addFilter( + new Ext.util.Filter({ + filterFn: (rec) => { + let ns = rec.data.ns; + return ns !== me.excludeNs && !ns.startsWith(`${me.excludeNs}/`); + }, + }), + ); + } + me.callParent(); }, }); diff --git a/www/window/NamespaceMove.js b/www/window/NamespaceMove.js new file mode 100644 index 00000000..415d509d --- /dev/null +++ b/www/window/NamespaceMove.js @@ -0,0 +1,79 @@ +Ext.define('PBS.window.NamespaceMove', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pbsNamespaceMove', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: 'storage-namespaces', + + submitText: gettext('Move'), + isCreate: true, + showTaskViewer: true, + + cbind: { + url: '/api2/extjs/admin/datastore/{datastore}/namespace', + title: (get) => Ext.String.format(gettext("Move Namespace '{0}'"), get('namespace')), + }, + method: 'PUT', + + width: 450, + fieldDefaults: { + labelWidth: 120, + }, + + cbindData: function (initialConfig) { + let ns = initialConfig.namespace ?? ''; + let parts = ns.split('/'); + return { nsName: parts[parts.length - 1] }; + }, + + // Returns the new-ns path that was submitted, for use by the caller after success. + getNewNamespace: function () { + let me = this; + let parent = me.down('[name=parent]').getValue() || ''; + let name = me.down('[name=name]').getValue(); + return parent ? `${parent}/${name}` : name; + }, + + items: { + xtype: 'inputpanel', + onGetValues: function (values) { + let parent = values.parent || ''; + let newNs = parent ? `${parent}/${values.name}` : values.name; + return { + ns: this.up('window').namespace, + 'new-ns': newNs, + }; + }, + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Namespace'), + cbind: { + value: '{namespace}', + }, + }, + { + xtype: 'pbsNamespaceSelector', + name: 'parent', + fieldLabel: gettext('New Parent'), + allowBlank: true, + cbind: { + datastore: '{datastore}', + excludeNs: '{namespace}', + }, + }, + { + xtype: 'proxmoxtextfield', + name: 'name', + fieldLabel: gettext('New Name'), + allowBlank: false, + maxLength: 31, + regex: PBS.Utils.SAFE_ID_RE, + regexText: gettext("Only alpha numerical, '_' and '-' (if not at start) allowed"), + cbind: { + value: '{nsName}', + }, + }, + ], + }, +}); -- 2.47.3