798 lines
17 KiB
JavaScript
798 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',
|
|
|
|
resizable: false,
|
|
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,
|
|
},
|
|
],
|
|
},
|
|
});
|