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:
parent
e01ca6a2dd
commit
4923a76f22
|
@ -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}`;
|
||||||
|
|
|
@ -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,26 +13,317 @@ 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());
|
||||||
},
|
},
|
||||||
|
|
||||||
referenceHolder: true,
|
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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
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: 'tabpanel',
|
||||||
|
reference: 'tabpanel',
|
||||||
|
layout: 'fit',
|
||||||
|
bodyPadding: 10,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: gettext('Snapshot Selection'),
|
||||||
xtype: 'inputpanel',
|
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(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
},
|
||||||
|
|
||||||
|
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) {
|
onGetValues: function(values) {
|
||||||
let me = this;
|
let me = this;
|
||||||
let datastores = [];
|
let datastores = [];
|
||||||
|
@ -46,49 +337,11 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
||||||
}
|
}
|
||||||
delete values.mapping;
|
delete values.mapping;
|
||||||
|
|
||||||
if (me.up('window').list !== undefined) {
|
|
||||||
values.snapshots = me.up('window').list;
|
|
||||||
}
|
|
||||||
|
|
||||||
values.store = datastores.join(',');
|
values.store = datastores.join(',');
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
},
|
},
|
||||||
|
|
||||||
column1: [
|
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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
column2: [
|
|
||||||
{
|
{
|
||||||
xtype: 'pbsUserSelector',
|
xtype: 'pbsUserSelector',
|
||||||
name: 'notify-user',
|
name: 'notify-user',
|
||||||
|
@ -109,9 +362,19 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
||||||
skipEmptyText: true,
|
skipEmptyText: true,
|
||||||
renderer: Ext.String.htmlEncode,
|
renderer: Ext.String.htmlEncode,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
column2: [
|
||||||
|
{
|
||||||
|
xtype: 'pbsDriveSelector',
|
||||||
|
fieldLabel: gettext('Drive'),
|
||||||
|
labelWidth: 120,
|
||||||
|
name: 'drive',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
xtype: 'pbsDataStoreSelector',
|
xtype: 'pbsDataStoreSelector',
|
||||||
fieldLabel: gettext('Target Datastore'),
|
fieldLabel: gettext('Target Datastore'),
|
||||||
|
labelWidth: 120,
|
||||||
reference: 'defaultDatastore',
|
reference: 'defaultDatastore',
|
||||||
name: 'store',
|
name: 'store',
|
||||||
listeners: {
|
listeners: {
|
||||||
|
@ -136,65 +399,18 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
||||||
xtype: 'pbsDataStoreMappingField',
|
xtype: 'pbsDataStoreMappingField',
|
||||||
reference: 'mappingGrid',
|
reference: 'mappingGrid',
|
||||||
name: 'mapping',
|
name: 'mapping',
|
||||||
|
height: 260,
|
||||||
defaultBindProperty: 'value',
|
defaultBindProperty: 'value',
|
||||||
hidden: true,
|
hidden: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
setDataStores: function(datastores) {
|
|
||||||
let me = this;
|
|
||||||
|
|
||||||
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() {
|
listeners: {
|
||||||
let me = this;
|
afterrender: 'updateSnapshots',
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue