public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] [PATCH proxmox-backup v4 3/5] ui: tape/window/TapeRestore: enabling selecting multiple snapshots
Date: Fri, 21 May 2021 12:20:20 +0200	[thread overview]
Message-ID: <20210521102022.26859-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20210521102022.26859-1-d.csapak@proxmox.com>

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>
---
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





  parent reply	other threads:[~2021-05-21 10:21 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-05-21 10:20 [pbs-devel] [PATCH proxmox-backup v4 0/5] ui: unify and improve tape restore window Dominik Csapak
2021-05-21 10:20 ` [pbs-devel] [PATCH proxmox-backup v4 1/5] ui: tape/TapeRestore: fix small DataStoreMappingGrid bugs Dominik Csapak
2021-05-21 10:20 ` [pbs-devel] [PATCH proxmox-backup v4 2/5] ui: tape/TapeRestore: improve SnapshotGrid Dominik Csapak
2021-05-21 10:20 ` Dominik Csapak [this message]
2021-05-21 10:20 ` [pbs-devel] [PATCH proxmox-backup v4 4/5] ui: tape/BackupOverview: also allow to filter by group for restore Dominik Csapak
2021-05-21 10:20 ` [pbs-devel] [PATCH proxmox-backup v4 5/5] ui: tape/BackupOverview: do not reload on restore Dominik Csapak
2021-05-21 14:34 ` [pbs-devel] applied-series: [PATCH proxmox-backup v4 0/5] ui: unify and improve tape restore window Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20210521102022.26859-4-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal