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 6AAC471C50 for ; Fri, 21 May 2021 12:21:03 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 5DBA4D1D5 for ; Fri, 21 May 2021 12:20:33 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (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 EA32BD157 for ; Fri, 21 May 2021 12:20:29 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 7C78842E48 for ; Fri, 21 May 2021 12:20:24 +0200 (CEST) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Fri, 21 May 2021 12:20:20 +0200 Message-Id: <20210521102022.26859-4-d.csapak@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210521102022.26859-1-d.csapak@proxmox.com> References: <20210521102022.26859-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.026 Adjusted score from AWL reputation of From: address 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 URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [data.store, content.store, node.data, entry.store, rec.data, values.store] Subject: [pbs-devel] [PATCH proxmox-backup v4 3/5] ui: tape/window/TapeRestore: enabling selecting multiple snapshots 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: Fri, 21 May 2021 10:21:03 -0000 by including the new snapshotselector. If a whole media-set is to be restored, select all snapshots to achieve this, we drop the 'restoreid' and 'datastores' properties for the restore window, and replace them by a 'prefilter' object (with 'store' and 'snapshot' properties) to be able to show the snapshots, we now have to always load the content of that media-set, so drop the short-circuit if we have the datastores already. change the layout of the restore window into a two-step window so that the first tab is the selection what to restore, and on the second tab the user chooses where to restore (drive, datastore, etc.) Signed-off-by: Dominik Csapak --- the controller has the basic structure of a generic wizard, but not all features we need yet for other uses. the plan is that when i have time in the next weeks, i'll use this class as a base for a generic wizard that i'll put into widget toolkit and rewrite our existing wizards using this the heights of the grid were found empirically, we sadly have no way currently to make a field fill the remaining height of an inputpanel i can look into that in the future www/tape/BackupOverview.js | 27 +- www/tape/window/TapeRestore.js | 530 +++++++++++++++++++++++---------- 2 files changed, 381 insertions(+), 176 deletions(-) diff --git a/www/tape/BackupOverview.js b/www/tape/BackupOverview.js index 0e105274..eb8ef907 100644 --- a/www/tape/BackupOverview.js +++ b/www/tape/BackupOverview.js @@ -19,27 +19,13 @@ Ext.define('PBS.TapeManagement.BackupOverview', { restore: function(view, rI, cI, item, e, rec) { let me = this; - let node = rec; - let mediaset = node.data.is_media_set ? node.data.text : node.data['media-set']; - let uuid = node.data['media-set-uuid']; - - let list; - let datastores; - if (node.data.restoreid !== undefined) { - list = [node.data.restoreid]; - datastores = [node.data.store]; - } else { - datastores = node.data.datastores; - while (!datastores && node.get('depth') > 2) { - node = node.parentNode; - datastores = node.data.datastores; - } - } + let mediaset = rec.data.is_media_set ? rec.data.text : rec.data['media-set']; + let uuid = rec.data['media-set-uuid']; + let prefilter = rec.data.prefilter; Ext.create('PBS.TapeManagement.TapeRestoreWindow', { mediaset, uuid, - datastores, - list, + prefilter, listeners: { destroy: function() { me.reload(); @@ -157,7 +143,10 @@ Ext.define('PBS.TapeManagement.BackupOverview', { entry.leaf = true; entry.children = []; entry['media-set'] = media_set; - entry.restoreid = `${entry.store}:${entry.snapshot}`; + entry.prefilter = { + store: entry.store, + snapshot: entry.snapshot, + }; let iconCls = PBS.Utils.get_type_icon_cls(entry.snapshot); if (iconCls !== '') { entry.iconCls = `fa ${iconCls}`; diff --git a/www/tape/window/TapeRestore.js b/www/tape/window/TapeRestore.js index 10624f9a..6bd35f53 100644 --- a/www/tape/window/TapeRestore.js +++ b/www/tape/window/TapeRestore.js @@ -1,11 +1,11 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { - extend: 'Proxmox.window.Edit', + extend: 'Ext.window.Window', alias: 'widget.pbsTapeRestoreWindow', mixins: ['Proxmox.Mixin.CBind'], width: 800, + height: 500, title: gettext('Restore Media Set'), - submitText: gettext('Restore'), url: '/api2/extjs/tape/restore', method: 'POST', showTaskViewer: true, @@ -13,188 +13,404 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { cbindData: function(config) { let me = this; - me.isSingle = false; - me.listText = ""; - if (me.list !== undefined) { - me.isSingle = true; - me.listText = me.list.join('
'); - me.title = gettext('Restore Snapshot'); + if (me.prefilter !== undefined) { + me.title = gettext('Restore Snapshot(s)'); } return {}; }, - defaults: { - labelWidth: 120, - }, + layout: 'fit', + bodyPadding: 0, - referenceHolder: true, + controller: { + xclass: 'Ext.app.ViewController', - items: [ - { - xtype: 'inputpanel', - - onGetValues: function(values) { - let me = this; - let datastores = []; - if (values.store.toString() !== "") { - datastores.push(values.store); - delete values.store; - } + panelIsValid: function(panel) { + return panel.query('[isFormField]').every(field => field.isValid()); + }, - if (values.mapping.toString() !== "") { - datastores.push(values.mapping); + checkValidity: function() { + let me = this; + let tabpanel = me.lookup('tabpanel'); + let items = tabpanel.items; + + let checkValidity = true; + + let indexOfActiveTab = items.indexOf(tabpanel.getActiveTab()); + let indexOfLastValidTab = 0; + + items.each((panel) => { + if (checkValidity) { + panel.setDisabled(false); + indexOfLastValidTab = items.indexOf(panel); + if (!me.panelIsValid(panel)) { + checkValidity = false; + } + } else { + panel.setDisabled(true); } - delete values.mapping; - if (me.up('window').list !== undefined) { - values.snapshots = me.up('window').list; - } + return true; + }); - values.store = datastores.join(','); + if (indexOfLastValidTab < indexOfActiveTab) { + tabpanel.setActiveTab(indexOfLastValidTab); + } else { + me.setButtonState(tabpanel.getActiveTab()); + } + }, - return values; - }, + setButtonState: function(panel) { + let me = this; + let isValid = me.panelIsValid(panel); + let nextButton = me.lookup('nextButton'); + let finishButton = me.lookup('finishButton'); + nextButton.setDisabled(!isValid); + finishButton.setDisabled(!isValid); + }, - 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: 'displayfield', - fieldLabel: gettext('Snapshot(s)'), - submitValue: false, - cbind: { - hidden: '{!isSingle}', - value: '{listText}', - }, - }, - { - xtype: 'pbsDriveSelector', - fieldLabel: gettext('Drive'), - name: 'drive', - }, - ], + changeButtonVisibility: function(tabpanel, newItem) { + let me = this; + let items = tabpanel.items; - 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('Target Datastore'), - reference: 'defaultDatastore', - name: 'store', - listeners: { - change: function(field, value) { - let me = this; - let grid = me.up('window').lookup('mappingGrid'); - grid.setNeedStores(!value); - }, - }, - }, - ], + let backButton = me.lookup('backButton'); + let nextButton = me.lookup('nextButton'); + let finishButton = me.lookup('finishButton'); - columnB: [ - { - fieldLabel: gettext('Datastore Mapping'), - labelWidth: 200, - hidden: true, - reference: 'mappingLabel', - xtype: 'displayfield', + let isLast = items.last() === newItem; + let isFirst = items.first() === newItem; + + backButton.setVisible(!isFirst); + nextButton.setVisible(!isLast); + finishButton.setVisible(isLast); + + me.setButtonState(newItem); + }, + + previousTab: function() { + let me = this; + let tabpanel = me.lookup('tabpanel'); + let index = tabpanel.items.indexOf(tabpanel.getActiveTab()); + tabpanel.setActiveTab(index - 1); + }, + + nextTab: function() { + let me = this; + let tabpanel = me.lookup('tabpanel'); + let index = tabpanel.items.indexOf(tabpanel.getActiveTab()); + tabpanel.setActiveTab(index + 1); + }, + + getValues: function() { + let me = this; + + let values = {}; + + let tabpanel = me.lookup('tabpanel'); + tabpanel + .query('inputpanel') + .forEach((panel) => + Proxmox.Utils.assemble_field_data(values, panel.getValues())); + + return values; + }, + + finish: function() { + let me = this; + let view = me.getView(); + + let values = me.getValues(); + let url = view.url; + let method = view.method; + + Proxmox.Utils.API2Request({ + url, + waitMsgTarget: view, + method, + params: values, + failure: function(response, options) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); }, - { - xtype: 'pbsDataStoreMappingField', - reference: 'mappingGrid', - name: 'mapping', - defaultBindProperty: 'value', - hidden: true, + success: function(response, options) { + // stay around so we can trigger our close events + // when background action is completed + view.hide(); + + Ext.create('Proxmox.window.TaskViewer', { + autoShow: true, + upid: response.result.data, + listeners: { + destroy: function() { + view.close(); + }, + }, + }); }, - ], + }); }, - ], - setDataStores: function(datastores) { - let me = this; + updateDatastores: function() { + let me = this; + let grid = me.lookup('snapshotGrid'); + let values = grid.getValue(); + if (values === 'all') { + values = []; + } + let datastores = {}; + values.forEach((snapshot) => { + const [datastore] = snapshot.split(':'); + datastores[datastore] = true; + }); - 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('Target Datastore')); - defaultField.setAllowBlank(false); - defaultField.setEmptyText(""); - return; - } + me.setDataStores(Object.keys(datastores)); + }, - label.setVisible(true); - defaultField.setFieldLabel(gettext('Default Datastore')); - defaultField.setAllowBlank(true); - defaultField.setEmptyText(Proxmox.Utils.NoneText); + setDataStores: function(datastores, initial) { + let me = this; - grid.setDataStores(datastores); - grid.setVisible(true); + // save all datastores on the first setting, and + // restore them if we selected all + if (initial) { + me.datastores = datastores; + } else if (datastores.length === 0) { + datastores = me.datastores; + } + + 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('Target 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); + }, + + updateSnapshots: function() { + let me = this; + let view = me.getView(); + let grid = me.lookup('snapshotGrid'); + + Proxmox.Utils.API2Request({ + waitMsgTarget: view, + url: `/tape/media/content?media-set=${view.uuid}`, + success: function(response, opt) { + let datastores = {}; + for (const content of response.result.data) { + datastores[content.store] = true; + } + me.setDataStores(Object.keys(datastores), true); + if (response.result.data.length > 0) { + grid.setDisabled(false); + grid.setVisible(true); + grid.getStore().setData(response.result.data); + grid.getSelectionModel().selectAll(); + // we've shown a big list, center the window again + view.center(); + } + }, + failure: function() { + // ignore failing api call, maybe catalog is missing + me.setDataStores([], true); + }, + }); + }, + + control: { + '[isFormField]': { + change: 'checkValidity', + validitychange: 'checkValidity', + }, + 'tabpanel': { + tabchange: 'changeButtonVisibility', + }, + }, }, - initComponent: function() { - let me = this; + buttons: [ + { + text: gettext('Back'), + reference: 'backButton', + handler: 'previousTab', + hidden: true, + }, + { + text: gettext('Next'), + reference: 'nextButton', + handler: 'nextTab', + }, + { + text: gettext('Restore'), + reference: 'finishButton', + handler: 'finish', + hidden: true, + }, + ], - 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; + items: [ + { + xtype: 'tabpanel', + reference: 'tabpanel', + layout: 'fit', + bodyPadding: 10, + items: [ + { + title: gettext('Snapshot Selection'), + xtype: 'inputpanel', + onGetValues: function(values) { + let me = this; + + if (values.snapshots === 'all') { + delete values.snapshots; + } else if (Ext.isString(values.snapshots) && values.snapshots) { + values.snapshots = values.snapshots.split(','); } - me.setDataStores(Object.keys(datastores)); + + return values; }, - failure: function() { - // ignore failing api call, maybe catalog is missing - me.setDataStores(); + + column1: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Media Set'), + cbind: { + value: '{mediaset}', + }, + }, + ], + + column2: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Media Set UUID'), + name: 'media-set', + submitValue: true, + cbind: { + value: '{uuid}', + }, + }, + ], + + columnB: [ + { + xtype: 'pbsTapeSnapshotGrid', + reference: 'snapshotGrid', + name: 'snapshots', + height: 322, + // will be shown/enabled on successful load + disabled: true, + hidden: true, + listeners: { + change: 'updateDatastores', + }, + cbind: { + prefilter: '{prefilter}', + }, + }, + ], + }, + { + title: gettext('Target'), + xtype: 'inputpanel', + onGetValues: function(values) { + let me = this; + let datastores = []; + if (values.store.toString() !== "") { + datastores.push(values.store); + delete values.store; + } + + if (values.mapping.toString() !== "") { + datastores.push(values.mapping); + } + delete values.mapping; + + values.store = datastores.join(','); + + return values; }, - }); - }, 10); - } + column1: [ + { + 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, + }, + ], + + column2: [ + { + xtype: 'pbsDriveSelector', + fieldLabel: gettext('Drive'), + labelWidth: 120, + name: 'drive', + }, + { + xtype: 'pbsDataStoreSelector', + fieldLabel: gettext('Target Datastore'), + labelWidth: 120, + 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', + height: 260, + defaultBindProperty: 'value', + hidden: true, + }, + ], + }, + ], + }, + ], + + listeners: { + afterrender: 'updateSnapshots', }, }); -- 2.20.1