ui: tape/window/TapeRestore: enabling selecting multiple snapshots

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>
This commit is contained in:
Dominik Csapak 2021-05-21 12:20:20 +02:00 committed by Thomas Lamprecht
parent e01ca6a2dd
commit 4923a76f22
2 changed files with 387 additions and 182 deletions

View File

@ -19,27 +19,13 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
restore: function(view, rI, cI, item, e, rec) { restore: function(view, rI, cI, item, e, rec) {
let me = this; let me = this;
let node = rec; let mediaset = rec.data.is_media_set ? rec.data.text : rec.data['media-set'];
let mediaset = node.data.is_media_set ? node.data.text : node.data['media-set']; let uuid = rec.data['media-set-uuid'];
let uuid = node.data['media-set-uuid']; let prefilter = rec.data.prefilter;
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;
}
}
Ext.create('PBS.TapeManagement.TapeRestoreWindow', { Ext.create('PBS.TapeManagement.TapeRestoreWindow', {
mediaset, mediaset,
uuid, uuid,
datastores, prefilter,
list,
listeners: { listeners: {
destroy: function() { destroy: function() {
me.reload(); me.reload();
@ -157,7 +143,10 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
entry.leaf = true; entry.leaf = true;
entry.children = []; entry.children = [];
entry['media-set'] = media_set; 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); let iconCls = PBS.Utils.get_type_icon_cls(entry.snapshot);
if (iconCls !== '') { if (iconCls !== '') {
entry.iconCls = `fa ${iconCls}`; entry.iconCls = `fa ${iconCls}`;

View File

@ -1,11 +1,11 @@
Ext.define('PBS.TapeManagement.TapeRestoreWindow', { Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
extend: 'Proxmox.window.Edit', extend: 'Ext.window.Window',
alias: 'widget.pbsTapeRestoreWindow', alias: 'widget.pbsTapeRestoreWindow',
mixins: ['Proxmox.Mixin.CBind'], mixins: ['Proxmox.Mixin.CBind'],
width: 800, width: 800,
height: 500,
title: gettext('Restore Media Set'), title: gettext('Restore Media Set'),
submitText: gettext('Restore'),
url: '/api2/extjs/tape/restore', url: '/api2/extjs/tape/restore',
method: 'POST', method: 'POST',
showTaskViewer: true, showTaskViewer: true,
@ -13,188 +13,404 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
cbindData: function(config) { cbindData: function(config) {
let me = this; let me = this;
me.isSingle = false; if (me.prefilter !== undefined) {
me.listText = ""; me.title = gettext('Restore Snapshot(s)');
if (me.list !== undefined) {
me.isSingle = true;
me.listText = me.list.join('<br>');
me.title = gettext('Restore Snapshot');
} }
return {}; return {};
}, },
defaults: { layout: 'fit',
labelWidth: 120, bodyPadding: 0,
controller: {
xclass: 'Ext.app.ViewController',
panelIsValid: function(panel) {
return panel.query('[isFormField]').every(field => field.isValid());
},
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);
}
return true;
});
if (indexOfLastValidTab < indexOfActiveTab) {
tabpanel.setActiveTab(indexOfLastValidTab);
} else {
me.setButtonState(tabpanel.getActiveTab());
}
},
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);
},
changeButtonVisibility: function(tabpanel, newItem) {
let me = this;
let items = tabpanel.items;
let backButton = me.lookup('backButton');
let nextButton = me.lookup('nextButton');
let finishButton = me.lookup('finishButton');
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);
},
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();
},
},
});
},
});
},
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;
});
me.setDataStores(Object.keys(datastores));
},
setDataStores: function(datastores, initial) {
let me = this;
// 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',
},
},
}, },
referenceHolder: true, 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,
},
],
items: [ items: [
{ {
xtype: 'inputpanel', xtype: 'tabpanel',
reference: 'tabpanel',
onGetValues: function(values) { layout: 'fit',
let me = this; bodyPadding: 10,
let datastores = []; items: [
if (values.store.toString() !== "") {
datastores.push(values.store);
delete values.store;
}
if (values.mapping.toString() !== "") {
datastores.push(values.mapping);
}
delete values.mapping;
if (me.up('window').list !== undefined) {
values.snapshots = me.up('window').list;
}
values.store = datastores.join(',');
return values;
},
column1: [
{ {
xtype: 'displayfield', title: gettext('Snapshot Selection'),
fieldLabel: gettext('Media Set'), xtype: 'inputpanel',
cbind: { onGetValues: function(values) {
value: '{mediaset}', let me = this;
if (values.snapshots === 'all') {
delete values.snapshots;
} else if (Ext.isString(values.snapshots) && values.snapshots) {
values.snapshots = values.snapshots.split(',');
}
return values;
}, },
},
{
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',
},
],
column2: [ column1: [
{ {
xtype: 'pbsUserSelector', xtype: 'displayfield',
name: 'notify-user', fieldLabel: gettext('Media Set'),
fieldLabel: gettext('Notify User'), cbind: {
emptyText: gettext('Current User'), value: '{mediaset}',
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);
}, },
}, ],
},
],
columnB: [ column2: [
{ {
fieldLabel: gettext('Datastore Mapping'), xtype: 'displayfield',
labelWidth: 200, fieldLabel: gettext('Media Set UUID'),
hidden: true, name: 'media-set',
reference: 'mappingLabel', submitValue: true,
xtype: 'displayfield', 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}',
},
},
],
}, },
{ {
xtype: 'pbsDataStoreMappingField', title: gettext('Target'),
reference: 'mappingGrid', xtype: 'inputpanel',
name: 'mapping', onGetValues: function(values) {
defaultBindProperty: 'value', let me = this;
hidden: true, 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;
},
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,
},
],
}, },
], ],
}, },
], ],
setDataStores: function(datastores) { listeners: {
let me = this; afterrender: 'updateSnapshots',
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);
},
initComponent: function() {
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);
}
}, },
}); });