for the case that the user selects only whole datastores, we do not want to send and (exhaustive) list of snapshots that get restored, but we only want to honor the mapping the user gives this avoids using the backup restore codepath that iterates twice over the tapes and would generally be slower for a lot of snapshots Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
794 lines
17 KiB
JavaScript
794 lines
17 KiB
JavaScript
Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
|
|
extend: 'Ext.window.Window',
|
|
alias: 'widget.pbsTapeRestoreWindow',
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
title: gettext('Restore Media-Set'),
|
|
|
|
width: 800,
|
|
height: 500,
|
|
|
|
url: '/api2/extjs/tape/restore',
|
|
method: 'POST',
|
|
|
|
showTaskViewer: true,
|
|
isCreate: true,
|
|
|
|
mediaset: undefined,
|
|
prefilter: undefined,
|
|
uuid: undefined,
|
|
|
|
cbindData: function(config) {
|
|
let me = this;
|
|
if (me.prefilter !== undefined) {
|
|
me.title = gettext('Restore Snapshot(s)');
|
|
}
|
|
return {};
|
|
},
|
|
|
|
layout: 'fit',
|
|
bodyPadding: 0,
|
|
|
|
viewModel: {
|
|
data: {
|
|
uuid: "",
|
|
singleDatastore: true,
|
|
},
|
|
formulas: {
|
|
singleSelectorLabel: get =>
|
|
get('singleDatastore') ? gettext('Target Datastore') : gettext('Default Datastore'),
|
|
singleSelectorEmptyText: get => get('singleDatastore') ? '' : Proxmox.Utils.NoneText,
|
|
},
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
panelIsValid: function(panel) {
|
|
return panel.query('[isFormField]').every(field => field.isValid());
|
|
},
|
|
|
|
changeMediaSet: function(field, value) {
|
|
let me = this;
|
|
let vm = me.getViewModel();
|
|
vm.set('uuid', value);
|
|
me.updateSnapshots();
|
|
},
|
|
|
|
checkValidity: function() {
|
|
let me = this;
|
|
|
|
let tabpanel = me.lookup('tabpanel');
|
|
if (!tabpanel) {
|
|
return; // can get triggered early, when the tabpanel is not yet available
|
|
}
|
|
let items = tabpanel.items;
|
|
|
|
let indexOfActiveTab = items.indexOf(tabpanel.getActiveTab());
|
|
let indexOfLastValidTab = 0;
|
|
|
|
let checkValidity = true;
|
|
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(grid, values) {
|
|
let me = this;
|
|
if (values === 'all') {
|
|
values = [];
|
|
}
|
|
let datastores = {};
|
|
values.forEach((snapshotOrDatastore) => {
|
|
let datastore = snapshotOrDatastore;
|
|
if (snapshotOrDatastore.indexOf(':') !== -1) {
|
|
let snapshot = snapshotOrDatastore;
|
|
let match = snapshot.split(':');
|
|
datastore = match[0];
|
|
} 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;
|
|
}
|
|
|
|
const singleDatastore = !datastores || datastores.length <= 1;
|
|
me.getViewModel().set('singleDatastore', singleDatastore);
|
|
|
|
let grid = me.lookup('mappingGrid');
|
|
if (!singleDatastore && grid) {
|
|
grid.setDataStores(datastores);
|
|
}
|
|
},
|
|
|
|
updateSnapshots: function() {
|
|
let me = this;
|
|
let view = me.getView();
|
|
let grid = me.lookup('snapshotGrid');
|
|
let vm = me.getViewModel();
|
|
let uuid = vm.get('uuid');
|
|
|
|
Proxmox.Utils.API2Request({
|
|
waitMsgTarget: view,
|
|
url: `/tape/media/content?media-set=${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.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);
|
|
},
|
|
});
|
|
},
|
|
|
|
init: function(view) {
|
|
let me = this;
|
|
let vm = me.getViewModel();
|
|
|
|
vm.set('uuid', view.uuid);
|
|
},
|
|
|
|
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: [
|
|
{
|
|
xtype: 'tabpanel',
|
|
reference: 'tabpanel',
|
|
layout: 'fit',
|
|
bodyPadding: 10,
|
|
items: [
|
|
{
|
|
title: gettext('Snapshot Selection'),
|
|
xtype: 'inputpanel',
|
|
onGetValues: function(values) {
|
|
let me = this;
|
|
|
|
if (values !== "all" &&
|
|
Ext.isString(values.snapshots) &&
|
|
values.snapshots &&
|
|
values.snapshots.indexOf(':') !== -1
|
|
) {
|
|
values.snapshots = values.snapshots.split(',');
|
|
} else {
|
|
delete values.snapshots;
|
|
}
|
|
|
|
return values;
|
|
},
|
|
|
|
column1: [
|
|
{
|
|
xtype: 'pbsMediaSetSelector',
|
|
fieldLabel: gettext('Media-Set'),
|
|
width: 350,
|
|
submitValue: false,
|
|
bind: {
|
|
value: '{uuid}',
|
|
},
|
|
cbind: {
|
|
hidden: '{uuid}',
|
|
disabled: '{uuid}',
|
|
},
|
|
listeners: {
|
|
change: 'changeMediaSet',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Media-Set'),
|
|
cbind: {
|
|
value: '{mediaset}',
|
|
hidden: '{!uuid}',
|
|
disabled: '{!uuid}',
|
|
},
|
|
},
|
|
],
|
|
|
|
column2: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Media-Set UUID'),
|
|
name: 'media-set',
|
|
submitValue: true,
|
|
bind: {
|
|
value: '{uuid}',
|
|
hidden: '{!uuid}',
|
|
disabled: '{!uuid}',
|
|
},
|
|
},
|
|
],
|
|
|
|
columnB: [
|
|
{
|
|
xtype: 'pbsTapeSnapshotGrid',
|
|
reference: 'snapshotGrid',
|
|
name: 'snapshots',
|
|
height: 322,
|
|
disabled: true, // will be shown/enabled on successful load
|
|
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;
|
|
},
|
|
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',
|
|
name: 'drive',
|
|
fieldLabel: gettext('Drive'),
|
|
labelWidth: 120,
|
|
},
|
|
{
|
|
xtype: 'pbsDataStoreSelector',
|
|
name: 'store',
|
|
labelWidth: 120,
|
|
bind: {
|
|
fieldLabel: '{singleSelectorLabel}',
|
|
emptyText: '{singleSelectorEmptyText}',
|
|
allowBlank: '{!singleDatastore}',
|
|
},
|
|
listeners: {
|
|
change: function(field, value) {
|
|
this.up('window').lookup('mappingGrid').setNeedStores(!value);
|
|
},
|
|
},
|
|
},
|
|
],
|
|
|
|
columnB: [
|
|
{
|
|
xtype: 'displayfield',
|
|
fieldLabel: gettext('Datastore Mapping'),
|
|
labelWidth: 200,
|
|
bind: {
|
|
hidden: '{singleDatastore}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pbsDataStoreMappingField',
|
|
name: 'mapping',
|
|
reference: 'mappingGrid',
|
|
height: 260,
|
|
defaultBindProperty: 'value',
|
|
bind: {
|
|
hidden: '{singleDatastore}',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
|
|
listeners: {
|
|
afterrender: 'updateSnapshots',
|
|
},
|
|
});
|
|
|
|
Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pbsDataStoreMappingField',
|
|
mixins: ['Ext.form.field.Field'],
|
|
|
|
scrollable: true,
|
|
|
|
getValue: function() {
|
|
let me = this;
|
|
let datastores = [];
|
|
me.getStore().each(rec => {
|
|
let { source, target } = rec.data;
|
|
if (target && target !== "") {
|
|
datastores.push(`${source}=${target}`);
|
|
}
|
|
});
|
|
|
|
return datastores.join(',');
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
needStores: false, // this determines if we need at least one valid mapping
|
|
},
|
|
formulas: {
|
|
emptyMeans: get => get('needStores') ? Proxmox.Utils.NoneText : Proxmox.Utils.defaultText,
|
|
},
|
|
},
|
|
|
|
setNeedStores: function(needStores) {
|
|
let me = this;
|
|
me.getViewModel().set('needStores', needStores);
|
|
me.checkChange();
|
|
me.validate();
|
|
},
|
|
|
|
setValue: function(value) {
|
|
let me = this;
|
|
me.setDataStores(value);
|
|
return me;
|
|
},
|
|
|
|
getErrors: function(value) {
|
|
let me = this;
|
|
let error = false;
|
|
|
|
if (me.getViewModel().get('needStores')) {
|
|
error = true;
|
|
me.getStore().each(rec => {
|
|
if (rec.data.target) {
|
|
error = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
let el = me.getActionEl();
|
|
if (error) {
|
|
me.addCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
|
|
let errorMsg = gettext("Need at least one mapping");
|
|
if (el) {
|
|
el.dom.setAttribute('data-errorqtip', errorMsg);
|
|
}
|
|
|
|
return [errorMsg];
|
|
}
|
|
me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
|
|
if (el) {
|
|
el.dom.setAttribute('data-errorqtip', "");
|
|
}
|
|
return [];
|
|
},
|
|
|
|
setDataStores: function(datastores) {
|
|
let me = this;
|
|
|
|
let data = [];
|
|
for (const datastore of datastores) {
|
|
data.push({
|
|
source: datastore,
|
|
target: '',
|
|
});
|
|
}
|
|
|
|
me.getStore().setData(data);
|
|
},
|
|
|
|
viewConfig: {
|
|
markDirty: false,
|
|
},
|
|
|
|
store: { data: [] },
|
|
|
|
columns: [
|
|
{
|
|
text: gettext('Source Datastore'),
|
|
dataIndex: 'source',
|
|
flex: 1,
|
|
},
|
|
{
|
|
text: gettext('Target Datastore'),
|
|
xtype: 'widgetcolumn',
|
|
dataIndex: 'target',
|
|
flex: 1,
|
|
widget: {
|
|
xtype: 'pbsDataStoreSelector',
|
|
allowBlank: true,
|
|
bind: {
|
|
emptyText: '{emptyMeans}',
|
|
},
|
|
listeners: {
|
|
change: function(selector, value) {
|
|
let me = this;
|
|
let rec = me.getWidgetRecord();
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
rec.set('target', value);
|
|
me.up('grid').checkChange();
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
Ext.define('PBS.TapeManagement.SnapshotGrid', {
|
|
extend: 'Ext.grid.Panel',
|
|
alias: 'widget.pbsTapeSnapshotGrid',
|
|
mixins: ['Ext.form.field.Field'],
|
|
|
|
getValue: function() {
|
|
let me = this;
|
|
let snapshots = [];
|
|
|
|
let storeCounts = {};
|
|
|
|
me.getSelection().forEach((rec) => {
|
|
let id = rec.get('id');
|
|
let store = rec.data.store;
|
|
let snap = rec.data.snapshot;
|
|
// only add if not filtered
|
|
if (me.store.findExact('id', id) !== -1) {
|
|
snapshots.push(`${store}:${snap}`);
|
|
if (storeCounts[store] === undefined) {
|
|
storeCounts[store] = 0;
|
|
}
|
|
storeCounts[store]++;
|
|
}
|
|
});
|
|
|
|
// getSource returns null if data is not filtered
|
|
let originalData = me.store.getData().getSource() || me.store.getData();
|
|
|
|
if (snapshots.length === originalData.length) {
|
|
return "all";
|
|
}
|
|
|
|
let wholeStores = [];
|
|
let wholeStoresSelected = true;
|
|
for (const [store, count] of Object.entries(storeCounts)) {
|
|
if (me.storeCounts[store] === count) {
|
|
wholeStores.push(store);
|
|
} else {
|
|
wholeStoresSelected = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (wholeStoresSelected) {
|
|
return wholeStores;
|
|
}
|
|
|
|
return snapshots;
|
|
},
|
|
|
|
setValue: function(value) {
|
|
let me = this;
|
|
// not implemented
|
|
return me;
|
|
},
|
|
|
|
getErrors: function(value) {
|
|
let me = this;
|
|
if (me.getSelection().length < 1) {
|
|
me.addCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
|
|
let errorMsg = gettext("Need at least one snapshot");
|
|
let el = me.getActionEl();
|
|
if (el) {
|
|
el.dom.setAttribute('data-errorqtip', errorMsg);
|
|
}
|
|
|
|
return [errorMsg];
|
|
}
|
|
me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
|
|
let el = me.getActionEl();
|
|
if (el) {
|
|
el.dom.setAttribute('data-errorqtip', "");
|
|
}
|
|
return [];
|
|
},
|
|
|
|
setData: function(records) {
|
|
let me = this;
|
|
let storeCounts = {};
|
|
records.forEach((rec) => {
|
|
let store = rec.store;
|
|
if (storeCounts[store] === undefined) {
|
|
storeCounts[store] = 0;
|
|
}
|
|
storeCounts[store]++;
|
|
});
|
|
me.storeCounts = storeCounts;
|
|
me.getStore().setData(records);
|
|
},
|
|
|
|
scrollable: true,
|
|
plugins: 'gridfilters',
|
|
|
|
viewConfig: {
|
|
emptyText: gettext('No Snapshots'),
|
|
markDirty: false,
|
|
},
|
|
|
|
selModel: 'checkboxmodel',
|
|
store: {
|
|
sorters: ['store', 'snapshot'],
|
|
data: [],
|
|
filters: [],
|
|
},
|
|
|
|
listeners: {
|
|
selectionchange: function() {
|
|
// to trigger validity and error checks
|
|
this.checkChange();
|
|
},
|
|
},
|
|
|
|
checkChangeEvents: [
|
|
'selectionchange',
|
|
'change',
|
|
],
|
|
|
|
columns: [
|
|
{
|
|
text: gettext('Source Datastore'),
|
|
dataIndex: 'store',
|
|
filter: {
|
|
type: 'list',
|
|
},
|
|
flex: 1,
|
|
},
|
|
{
|
|
text: gettext('Snapshot'),
|
|
dataIndex: 'snapshot',
|
|
filter: {
|
|
type: 'string',
|
|
},
|
|
flex: 2,
|
|
},
|
|
],
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
me.callParent();
|
|
if (me.prefilter !== undefined) {
|
|
me.store.filters.add(
|
|
{
|
|
id: 'x-gridfilter-store',
|
|
property: 'store',
|
|
operator: 'in',
|
|
value: [me.prefilter.store],
|
|
},
|
|
{
|
|
id: 'x-gridfilter-snapshot',
|
|
property: 'snapshot',
|
|
value: me.prefilter.snapshot,
|
|
},
|
|
);
|
|
}
|
|
|
|
me.mon(me.store, 'filterchange', () => me.checkChange());
|
|
},
|
|
});
|
|
|
|
Ext.define('PBS.TapeManagement.MediaSetSelector', {
|
|
extend: 'Proxmox.form.ComboGrid',
|
|
alias: 'widget.pbsMediaSetSelector',
|
|
|
|
allowBlank: false,
|
|
displayField: 'media-set-name',
|
|
valueField: 'media-set-uuid',
|
|
autoSelect: false,
|
|
|
|
store: {
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: '/api2/json/tape/media/media-sets',
|
|
},
|
|
autoLoad: true,
|
|
idProperty: 'media-set-uuid',
|
|
sorters: ['pool', 'media-set-ctime'],
|
|
},
|
|
|
|
listConfig: {
|
|
width: 600,
|
|
columns: [
|
|
{
|
|
text: gettext('Pool'),
|
|
dataIndex: 'pool',
|
|
flex: 1,
|
|
},
|
|
{
|
|
text: gettext('Name'),
|
|
dataIndex: 'media-set-name',
|
|
width: 180,
|
|
},
|
|
{
|
|
text: gettext('Media-Set UUID'),
|
|
dataIndex: 'media-set-uuid',
|
|
width: 280,
|
|
},
|
|
],
|
|
},
|
|
});
|