proxmox-backup/www/tape/window/TapeRestore.js

797 lines
17 KiB
JavaScript
Raw Normal View History

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',
modal: 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) {
// keep around so we can trigger our close events when background action completes
view.hide();
Ext.create('Proxmox.window.TaskViewer', {
autoShow: true,
upid: response.result.data,
listeners: {
destroy: () => 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.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,
emptyText: gettext('Select Media-Set to restore'),
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
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',
isFormField: false,
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) {
if (me.prefilter.store !== undefined) {
me.store.filters.add(
{
id: 'x-gridfilter-store',
property: 'store',
operator: 'in',
value: [me.prefilter.store],
},
);
}
if (me.prefilter.snapshot !== undefined) {
me.store.filters.add(
{
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,
},
],
},
});