]> git.proxmox.com Git - proxmox-backup.git/commitdiff
ui: tape/window/TapeRestore: enabling selecting multiple snapshots
authorDominik Csapak <d.csapak@proxmox.com>
Fri, 21 May 2021 10:20:20 +0000 (12:20 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Fri, 21 May 2021 11:25:45 +0000 (13:25 +0200)
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 <d.csapak@proxmox.com>
www/tape/BackupOverview.js
www/tape/window/TapeRestore.js

index 0e105274b9917c4b6718148221627c7da60b1cd9..eb8ef90739886c8d87295efe3e9f3212a88c8b61 100644 (file)
@@ -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}`;
index 10624f9adfdd0a54d0c498da5f645919be55555c..6bd35f53abcbe40e896df04744056f7f90048d27 100644 (file)
@@ -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('<br>');
-           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',
     },
 });