000e6cad5c
since extjs 7.0 those will get picked up by our query logic and sent to the backend. prevent that by setting isFormField to false (we assemble the values differently) Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
797 lines
17 KiB
JavaScript
797 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',
|
|
|
|
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,
|
|
},
|
|
],
|
|
},
|
|
});
|