[pbs-devel] [PATCH proxmox-backup v4 3/5] ui: tape/window/TapeRestore: enabling selecting multiple snapshots

Dominik Csapak d.csapak at proxmox.com
Fri May 21 12:20:20 CEST 2021


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 at proxmox.com>
---
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('<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',
     },
 });
 
-- 
2.20.1






More information about the pbs-devel mailing list