From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 537116A5BB for ; Thu, 25 Mar 2021 11:05:14 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 4A0E31B4B1 for ; Thu, 25 Mar 2021 11:04:44 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id A28F91B4A3 for ; Thu, 25 Mar 2021 11:04:42 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 6DFD046472 for ; Thu, 25 Mar 2021 11:04:42 +0100 (CET) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Thu, 25 Mar 2021 11:04:40 +0100 Message-Id: <20210325100441.7770-1-d.csapak@proxmox.com> X-Mailer: git-send-email 2.20.1 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.178 Adjusted score from AWL reputation of From: address KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [node.data, values.store, content.store] Subject: [pbs-devel] [PATCH proxmox-backup 1/2] tape: ui: TapeRestore: make datastore mapping selectable X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Thu, 25 Mar 2021 10:05:14 -0000 by adding a custom field (grid) where the user can select a target datastore for each source datastore on tape if we have not loaded the content of the media set yet, we have to load it on window open to get the list of datastores on the tape Signed-off-by: Dominik Csapak --- www/tape/BackupOverview.js | 13 +- www/tape/window/TapeRestore.js | 302 ++++++++++++++++++++++++++++----- 2 files changed, 272 insertions(+), 43 deletions(-) diff --git a/www/tape/BackupOverview.js b/www/tape/BackupOverview.js index 0f9a35af..0cc0e18e 100644 --- a/www/tape/BackupOverview.js +++ b/www/tape/BackupOverview.js @@ -24,11 +24,18 @@ Ext.define('PBS.TapeManagement.BackupOverview', { return; } - let mediaset = selection[0].data.text; - let uuid = selection[0].data['media-set-uuid']; + let node = selection[0]; + let mediaset = node.data.text; + let uuid = node.data['media-set-uuid']; + let datastores = node.data.datastores; + while (!datastores && node.get('depth') > 2) { + node = node.parentNode; + datastores = node.data.datastores; + } Ext.create('PBS.TapeManagement.TapeRestoreWindow', { mediaset, uuid, + datastores, listeners: { destroy: function() { me.reload(); @@ -185,6 +192,7 @@ Ext.define('PBS.TapeManagement.BackupOverview', { } let storeList = Object.values(stores); + let storeNameList = Object.keys(stores); let expand = storeList.length === 1; for (const store of storeList) { store.children = Object.values(store.tapes); @@ -198,6 +206,7 @@ Ext.define('PBS.TapeManagement.BackupOverview', { } node.set('loaded', true); + node.set('datastores', storeNameList); Proxmox.Utils.setErrorMask(view, false); node.expand(); } catch (error) { diff --git a/www/tape/window/TapeRestore.js b/www/tape/window/TapeRestore.js index 6b356bda..a9deb745 100644 --- a/www/tape/window/TapeRestore.js +++ b/www/tape/window/TapeRestore.js @@ -1,9 +1,9 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { extend: 'Proxmox.window.Edit', - alias: 'pbsTapeRestoreWindow', + alias: 'widget.pbsTapeRestoreWindow', mixins: ['Proxmox.Mixin.CBind'], - width: 400, + width: 800, title: gettext('Restore Media Set'), url: '/api2/extjs/tape/restore', method: 'POST', @@ -14,52 +14,272 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { labelWidth: 120, }, + referenceHolder: true, + items: [ { - xtype: 'displayfield', - fieldLabel: gettext('Media Set'), - cbind: { - value: '{mediaset}', - }, - }, - { - xtype: 'displayfield', - fieldLabel: gettext('Media Set UUID'), - name: 'media-set', - submitValue: true, - cbind: { - value: '{uuid}', + xtype: 'inputpanel', + + onGetValues: function(values) { + let me = this; + let datastores = []; + if (values.store && values.store !== "") { + datastores.push(values.store); + delete values.store; + } + + if (values.mapping) { + datastores.push(values.mapping); + delete values.mapping; + } + + values.store = datastores.join(','); + + return values; }, + + column1: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Media Set'), + cbind: { + value: '{mediaset}', + }, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Media Set UUID'), + name: 'media-set', + submitValue: true, + cbind: { + value: '{uuid}', + }, + }, + { + xtype: 'pbsDriveSelector', + fieldLabel: gettext('Drive'), + name: 'drive', + }, + ], + + column2: [ + { + xtype: 'pbsUserSelector', + name: 'notify-user', + fieldLabel: gettext('Notify User'), + emptyText: gettext('Current User'), + value: null, + allowBlank: true, + skipEmptyText: true, + renderer: Ext.String.htmlEncode, + }, + { + xtype: 'pbsUserSelector', + name: 'owner', + fieldLabel: gettext('Owner'), + emptyText: gettext('Current User'), + value: null, + allowBlank: true, + skipEmptyText: true, + renderer: Ext.String.htmlEncode, + }, + { + xtype: 'pbsDataStoreSelector', + fieldLabel: gettext('Datastore'), + reference: 'defaultDatastore', + name: 'store', + listeners: { + change: function(field, value) { + let me = this; + let grid = me.up('window').lookup('mappingGrid'); + grid.setNeedStores(!value); + }, + }, + }, + ], + + columnB: [ + { + fieldLabel: gettext('Datastore Mapping'), + labelWidth: 200, + hidden: true, + reference: 'mappingLabel', + xtype: 'displayfield', + }, + { + xtype: 'pbsDataStoreMappingField', + reference: 'mappingGrid', + name: 'mapping', + defaultBindProperty: 'value', + hidden: true, + }, + ], }, + ], + + setDataStores: function(datastores) { + let me = this; + + let label = me.lookup('mappingLabel'); + let grid = me.lookup('mappingGrid'); + let defaultField = me.lookup('defaultDatastore'); + + if (!datastores || datastores.length <= 1) { + label.setVisible(false); + grid.setVisible(false); + defaultField.setFieldLabel(gettext('Datastore')); + defaultField.setAllowBlank(false); + defaultField.setEmptyText(""); + return; + } + + label.setVisible(true); + defaultField.setFieldLabel(gettext('Default Datastore')); + defaultField.setAllowBlank(true); + defaultField.setEmptyText(Proxmox.Utils.NoneText); + + grid.setDataStores(datastores); + grid.setVisible(true); + }, + + initComponent: function() { + let me = this; + + me.callParent(); + if (me.datastores) { + me.setDataStores(me.datastores); + } else { + // use timeout so that the window is rendered already + // for correct masking + setTimeout(function() { + Proxmox.Utils.API2Request({ + waitMsgTarget: me, + url: `/tape/media/content?media-set=${me.uuid}`, + success: function(response, opt) { + let datastores = {}; + for (const content of response.result.data) { + datastores[content.store] = true; + } + me.setDataStores(Object.keys(datastores)); + }, + failure: function() { + // ignore failing api call, maybe catalog is missing + me.setDataStores(); + }, + }); + }, 10); + } + }, +}); + +Ext.define('PBS.TapeManagement.DataStoreMappingGrid', { + extend: 'Ext.grid.Panel', + alias: 'widget.pbsDataStoreMappingField', + mixins: ['Ext.form.field.Field'], + + getValue: function() { + let me = this; + let datastores = []; + me.getStore().each((rec) => { + let source = rec.data.source; + let target = rec.data.target; + if (target && target !== "") { + datastores.push(`${source}=${target}`); + } + }); + + return datastores.join(','); + }, + + // this determines if we need at least one valid mapping + needStores: false, + + setNeedStores: function(needStores) { + let me = this; + me.needStores = needStores; + me.checkChange(); + me.validate(); + }, + + setValue: function(value) { + let me = this; + me.setDataStores(value); + return me; + }, + + getErrors: function(value) { + let me = this; + let error = false; + + if (me.needStores) { + error = true; + me.getStore().each((rec) => { + if (rec.data.target) { + error = false; + } + }); + } + + if (error) { + me.addCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']); + let errorMsg = gettext("Need at least one mapping"); + me.getActionEl().dom.setAttribute('data-errorqtip', errorMsg); + + return [errorMsg]; + } + me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']); + me.getActionEl().dom.setAttribute('data-errorqtip', ""); + return []; + }, + + setDataStores: function(datastores) { + let me = this; + let store = me.getStore(); + let data = []; + + for (const datastore of datastores) { + data.push({ + source: datastore, + target: '', + }); + } + + store.setData(data); + }, + + viewConfig: { + markDirty: false, + }, + + store: { data: [] }, + + columns: [ { - xtype: 'pbsDataStoreSelector', - fieldLabel: gettext('Datastore'), - name: 'store', - }, - { - xtype: 'pbsDriveSelector', - fieldLabel: gettext('Drive'), - name: 'drive', - }, - { - xtype: 'pbsUserSelector', - name: 'notify-user', - fieldLabel: gettext('Notify User'), - emptyText: gettext('Current User'), - value: null, - allowBlank: true, - skipEmptyText: true, - renderer: Ext.String.htmlEncode, + text: gettext('Source Datastore'), + dataIndex: 'source', + flex: 1, }, { - xtype: 'pbsUserSelector', - name: 'owner', - fieldLabel: gettext('Owner'), - emptyText: gettext('Current User'), - value: null, - allowBlank: true, - skipEmptyText: true, - renderer: Ext.String.htmlEncode, + text: gettext('Target Datastore'), + xtype: 'widgetcolumn', + dataIndex: 'target', + flex: 1, + widget: { + xtype: 'pbsDataStoreSelector', + allowBlank: true, + emptyText: Proxmox.Utils.NoneText, + listeners: { + change: function(selector, value) { + let me = this; + let rec = me.getWidgetRecord(); + if (!rec) { + return; + } + rec.set('target', value); + me.up('grid').checkChange(); + }, + }, + }, }, ], }); -- 2.20.1