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 9B4B87B574 for ; Wed, 12 May 2021 13:19:02 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 9074BC7F3 for ; Wed, 12 May 2021 13:19:02 +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) server-digest SHA256) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 5F593C7E9 for ; Wed, 12 May 2021 13:19:01 +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 351F546558 for ; Wed, 12 May 2021 13:19:01 +0200 (CEST) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Wed, 12 May 2021 13:18:59 +0200 Message-Id: <20210512111859.2936-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.018 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 Subject: [pbs-devel] [PATCH proxmox-backup] ui: tape/BackupOverview: unify and improve tape restore window 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: Wed, 12 May 2021 11:19:02 -0000 adds a snapshot list to the restore window where the user can select distinct snapshots to restore this also replaces the restore / restore single button by inline actions for the media-set, backup groups and snapshots when clicking the action for a group/snapshot, the snapshotselector gets activated and preselected/filtered so that it includes those snapshots (can be overridden from the user) this change means that we now load the media set everytime we open the window, since we want to have the list of snapshots. (we could build that from the tree again, or hold it there somewhere as list, but i do not think we gain much with that, and it simplifies the datastore selection code a bit) Signed-off-by: Dominik Csapak --- this replaces my patch 'ui: tape/BackupOverview: add 'restore partial' button' www/tape/BackupOverview.js | 81 ++++--------- www/tape/window/TapeRestore.js | 211 ++++++++++++++++++++++++++------- 2 files changed, 191 insertions(+), 101 deletions(-) diff --git a/www/tape/BackupOverview.js b/www/tape/BackupOverview.js index c028d58d..af32d2ce 100644 --- a/www/tape/BackupOverview.js +++ b/www/tape/BackupOverview.js @@ -16,58 +16,17 @@ Ext.define('PBS.TapeManagement.BackupOverview', { }).show(); }, - restoreSingle: function(button, record) { + restore: function(view, rI, cI, item, e, rec) { let me = this; - let view = me.getView(); - let selection = view.getSelection(); - if (!selection || selection.length < 1) { - return; - } - - let node = selection[0]; - if (node.data.restoreid === undefined) { - return; - } - let restoreid = node.data.restoreid; - let mediaset = node.data.text; - let uuid = node.data['media-set-uuid']; - let datastores = [node.data.store]; - - Ext.create('PBS.TapeManagement.TapeRestoreWindow', { - mediaset, - uuid, - list: [ - restoreid, - ], - datastores, - listeners: { - destroy: function() { - me.reload(); - }, - }, - }).show(); - }, - - restore: function(button, record) { - let me = this; - let view = me.getView(); - let selection = view.getSelection(); - if (!selection || selection.length < 1) { - return; - } - let node = selection[0]; + let node = rec; 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; - } + let restoreid = node.data.restoreid; Ext.create('PBS.TapeManagement.TapeRestoreWindow', { mediaset, uuid, - datastores, + prefilter: restoreid, listeners: { destroy: function() { me.reload(); @@ -99,6 +58,7 @@ Ext.define('PBS.TapeManagement.BackupOverview', { if (data[pool][media_set] === undefined) { data[pool][media_set] = entry; data[pool][media_set].text = media_set; + data[pool][media_set].restore =true; data[pool][media_set].tapes = 1; data[pool][media_set]['seq-nr'] = undefined; data[pool][media_set].is_media_set = true; @@ -179,6 +139,7 @@ Ext.define('PBS.TapeManagement.BackupOverview', { for (let entry of list.result.data) { entry.text = entry.snapshot; + entry.restore = true; entry.leaf = true; entry.children = []; entry.restoreid = `${entry.store}:${entry.snapshot}`; @@ -217,6 +178,8 @@ Ext.define('PBS.TapeManagement.BackupOverview', { text, 'media-set-uuid': entry['media-set-uuid'], leaf: false, + restore: true, + restoreid: `${store}:${text}`, iconCls: `fa ${iconCls}`, children: [], }); @@ -287,22 +250,6 @@ Ext.define('PBS.TapeManagement.BackupOverview', { text: gettext('New Backup'), handler: 'backup', }, - { - xtype: 'proxmoxButton', - disabled: true, - text: gettext('Restore Media Set'), - handler: 'restore', - parentXType: 'treepanel', - enableFn: (rec) => !!rec.data['media-set-uuid'], - }, - { - xtype: 'proxmoxButton', - disabled: true, - text: gettext('Restore Snapshot'), - handler: 'restoreSingle', - parentXType: 'treepanel', - enableFn: (rec) => !!rec.data.restoreid, - }, ], columns: [ @@ -313,6 +260,18 @@ Ext.define('PBS.TapeManagement.BackupOverview', { sortable: false, flex: 3, }, + { + header: gettext('Actions'), + xtype: 'actioncolumn', + items: [ + { + handler: 'restore', + tooltip: gettext('Restore'), + getClass: (v, m, rec) => rec.data.restore ? 'fa fa-fw fa-undo' : 'pmx-hidden', + isDisabled: (v, r, c, i, rec) => !rec.data.restore, + }, + ], + }, { text: gettext('Tapes'), dataIndex: 'tapes', diff --git a/www/tape/window/TapeRestore.js b/www/tape/window/TapeRestore.js index 7e4f5cae..352c47db 100644 --- a/www/tape/window/TapeRestore.js +++ b/www/tape/window/TapeRestore.js @@ -10,14 +10,13 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { showTaskViewer: true, isCreate: true, + allowSnapshots: false, + 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.allowSnapshots = true; + me.title = gettext('Restore Snapshot(s)'); } return {}; }, @@ -45,8 +44,8 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { } delete values.mapping; - if (me.up('window').list !== undefined) { - values.snapshots = me.up('window').list; + if (Ext.isString(values.snapshots)) { + values.snapshots = values.snapshots.split(','); } values.store = datastores.join(','); @@ -71,15 +70,6 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { value: '{uuid}', }, }, - { - xtype: 'displayfield', - fieldLabel: gettext('Snapshot(s)'), - submitValue: false, - cbind: { - hidden: '{!isSingle}', - value: '{listText}', - }, - }, { xtype: 'pbsDriveSelector', fieldLabel: gettext('Drive'), @@ -138,6 +128,30 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { defaultBindProperty: 'value', hidden: true, }, + { + boxLabel: gettext('Snapshot Selection'), + labelWidth: 200, + xtype: 'proxmoxcheckbox', + isFormField: false, + cbind: { + value: '{allowSnapshots}', + }, + listeners: { + change: function(cb, value) { + let me = this; + me.lookup('snapshotGrid').setDisabled(!value); + }, + }, + }, + { + xtype: 'pbsTapeSnapshotGrid', + reference: 'snapshotGrid', + name: 'snapshots', + cbind: { + disabled: '{!allowSnapshots}', + prefilter: '{prefilter}', + }, + }, ], }, ], @@ -171,29 +185,39 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', { 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); - } + // 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)); + let grid = me.lookup('snapshotGrid'); + let store = grid.getStore(); + store.setData(response.result.data); + if (me.prefilter !== undefined) { + store.each((rec) => { + let restoreid = `${rec.data.store}:${rec.data.snapshot}`; + if (restoreid.indexOf(me.prefilter) !== -1) { + rec.set('include', true); + } + }); + + grid.setFilter(me.prefilter); + } + store.sort('snapshot'); + }, + failure: function() { + // ignore failing api call, maybe catalog is missing + me.setDataStores(); + }, + }); + }, 10); }, }); @@ -202,6 +226,10 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', { alias: 'widget.pbsDataStoreMappingField', mixins: ['Ext.form.field.Field'], + maxHeight: 150, + scrollable: true, + margin: '0 0 10 0', + getValue: function() { let me = this; let datastores = []; @@ -308,3 +336,106 @@ Ext.define('PBS.TapeManagement.DataStoreMappingGrid', { }, ], }); + +Ext.define('PBS.TapeManagement.SnapshotGrid', { + extend: 'Ext.grid.Panel', + alias: 'widget.pbsTapeSnapshotGrid', + mixins: ['Ext.form.field.Field'], + + getValue: function() { + let me = this; + let snapshots = []; + + me.getStore().each((rec) => { + if (rec.data.include) { + let store = rec.data.store; + let snap = rec.data.snapshot; + snapshots.push(`${store}:${snap}`); + } + }); + + return snapshots; + }, + + setValue: function(value) { + let me = this; + // not implemented + return me; + }, + + getErrors: function(value) { + let me = this; + let firstSelected = me.getStore().findBy((rec) => !!rec.data.include); + + if (firstSelected === -1) { + me.addCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']); + let errorMsg = gettext("Need at least one snapshot"); + 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 []; + }, + + scrollable: true, + height: 200, + + viewConfig: { + emptyText: gettext('No Snapshots'), + markDirty: false, + }, + + setFilter: function(value) { + let me = this; + me.down('textfield').setValue(value); + }, + + tbar: [ + { + fieldLabel: gettext('Filter'), + xtype: 'textfield', + isFormField: false, + listeners: { + change: function(field, value) { + let me = this; + let grid = me.up('grid'); + let store = grid.getStore(); + store.clearFilter(); + store.filterBy((rec) => { + let restoreid = `${rec.data.store}:${rec.data.snapshot}`; + return restoreid.indexOf(value) !== -1; + }); + grid.checkChange(); + }, + }, + }, + ], + + store: { data: [] }, + + columns: [ + { + xtype: 'checkcolumn', + text: gettext('Include'), + dataIndex: 'include', + listeners: { + checkchange: function(cb, value) { + let grid = this.up('grid'); + grid.checkChange(); + }, + }, + }, + { + text: gettext('Source Datastore'), + dataIndex: 'store', + flex: 1, + }, + { + text: gettext('Snapshot'), + dataIndex: 'snapshot', + flex: 4, + }, + ], +}); -- 2.20.1