Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
- extend: 'Proxmox.window.Edit',
+ extend: 'Ext.window.Window',
alias: 'widget.pbsTapeRestoreWindow',
mixins: ['Proxmox.Mixin.CBind'],
+ title: gettext('Restore Media-Set'),
+
width: 800,
- title: gettext('Restore Media Set'),
- submitText: gettext('Restore'),
+ height: 500,
+
url: '/api2/extjs/tape/restore',
method: 'POST',
- showTaskViewer: true,
- isCreate: true,
+
+ resizable: false,
+ modal: true,
+
+ mediaset: undefined,
+ prefilter: undefined,
+ uuid: undefined,
cbindData: function(config) {
let me = this;
- me.isSingle = false;
- me.listText = "";
- if (me.list !== undefined) {
- me.isSingle = true;
- me.listText = me.list.join('<br>');
- me.title = gettext('Restore Snapshot');
+ if (me.prefilter !== undefined) {
+ me.title = gettext('Restore Snapshot(s)');
}
return {};
},
- defaults: {
- labelWidth: 120,
+ 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,
+ },
},
- referenceHolder: true,
+ controller: {
+ xclass: 'Ext.app.ViewController',
- items: [
- {
- xtype: 'inputpanel',
-
- onGetValues: function(values) {
- let me = this;
- let datastores = [];
- if (values.store.toString() !== "") {
- datastores.push(values.store);
- delete values.store;
- }
+ panelIsValid: function(panel) {
+ return panel.query('[isFormField]').every(field => field.isValid());
+ },
- if (values.mapping.toString() !== "") {
- datastores.push(values.mapping);
- }
- delete values.mapping;
+ changeMediaSet: function(field, value) {
+ let me = this;
+ let vm = me.getViewModel();
+ vm.set('uuid', value);
+ me.updateSnapshots();
+ },
- if (me.up('window').list !== undefined) {
- values.snapshots = me.up('window').list;
+ 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);
}
- values.store = datastores.join(',');
+ return true;
+ });
- return values;
- },
+ if (indexOfLastValidTab < indexOfActiveTab) {
+ tabpanel.setActiveTab(indexOfLastValidTab);
+ } else {
+ me.setButtonState(tabpanel.getActiveTab());
+ }
+ },
- 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',
- },
- ],
+ 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);
+ },
- column2: [
- {
- 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,
+ 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);
},
- {
- xtype: 'pbsDataStoreSelector',
- fieldLabel: gettext('Target Datastore'),
- reference: 'defaultDatastore',
- name: 'store',
- listeners: {
- change: function(field, value) {
- let me = this;
- let grid = me.up('window').lookup('mappingGrid');
- grid.setNeedStores(!value);
- },
- },
+ 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(),
+ },
+ });
},
- ],
+ });
+ },
- columnB: [
- {
- fieldLabel: gettext('Datastore Mapping'),
- labelWidth: 200,
- hidden: true,
- reference: 'mappingLabel',
- xtype: 'displayfield',
+ 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();
+ }
},
- {
- xtype: 'pbsDataStoreMappingField',
- reference: 'mappingGrid',
- name: 'mapping',
- defaultBindProperty: 'value',
- hidden: true,
+ failure: function() {
+ // ignore failing api call, maybe catalog is missing
+ me.setDataStores([], true);
},
- ],
+ });
},
- ],
- setDataStores: function(datastores) {
- let me = this;
+ init: function(view) {
+ let me = this;
+ let vm = me.getViewModel();
- 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);
+ vm.set('uuid', view.uuid);
+ },
- grid.setDataStores(datastores);
- grid.setVisible(true);
+ control: {
+ '[isFormField]': {
+ change: 'checkValidity',
+ validitychange: 'checkValidity',
+ },
+ 'tabpanel': {
+ tabchange: 'changeButtonVisibility',
+ },
+ },
},
- initComponent: function() {
- let me = this;
+ 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,
+ },
+ ],
- 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;
+ 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;
}
- me.setDataStores(Object.keys(datastores));
+
+ return values;
},
- failure: function() {
- // ignore failing api call, maybe catalog is missing
- me.setDataStores();
+
+ 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;
},
- });
- }, 10);
- }
+ 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',
},
});
alias: 'widget.pbsDataStoreMappingField',
mixins: ['Ext.form.field.Field'],
+ scrollable: true,
+
getValue: function() {
let me = this;
let datastores = [];
- me.getStore().each((rec) => {
- let source = rec.data.source;
- let target = rec.data.target;
+ me.getStore().each(rec => {
+ let { source, target } = rec.data;
if (target && target !== "") {
datastores.push(`${source}=${target}`);
}
return datastores.join(',');
},
- // this determines if we need at least one valid mapping
- needStores: false,
+ 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.needStores = needStores;
+ me.getViewModel().set('needStores', needStores);
me.checkChange();
me.validate();
},
let me = this;
let error = false;
- if (me.needStores) {
+ if (me.getViewModel().get('needStores')) {
error = true;
- me.getStore().each((rec) => {
+ 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");
- me.getActionEl().dom.setAttribute('data-errorqtip', errorMsg);
+ if (el) {
+ el.dom.setAttribute('data-errorqtip', errorMsg);
+ }
return [errorMsg];
}
me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
- me.getActionEl().dom.setAttribute('data-errorqtip', "");
+ if (el) {
+ el.dom.setAttribute('data-errorqtip', "");
+ }
return [];
},
setDataStores: function(datastores) {
let me = this;
- let store = me.getStore();
- let data = [];
+ let data = [];
for (const datastore of datastores) {
data.push({
source: datastore,
});
}
- store.setData(data);
+ me.getStore().setData(data);
},
viewConfig: {
flex: 1,
widget: {
xtype: 'pbsDataStoreSelector',
+ isFormField: false,
allowBlank: true,
- emptyText: Proxmox.Utils.NoneText,
+ bind: {
+ emptyText: '{emptyMeans}',
+ },
listeners: {
change: function(selector, value) {
let me = this;
let me = this;
let snapshots = [];
+ let storeCounts = {};
+
me.getSelection().forEach((rec) => {
let id = rec.get('id');
let store = rec.data.store;
// 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;
},
getErrors: function(value) {
let me = this;
- if (me.getSelection() < 1) {
+ 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");
- me.getActionEl().dom.setAttribute('data-errorqtip', errorMsg);
+ 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']);
- me.getActionEl().dom.setAttribute('data-errorqtip', "");
+ 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,
- height: 350,
plugins: 'gridfilters',
viewConfig: {
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,
- },
- );
+ 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,
+ },
+ ],
},
});