Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
- extend: 'Proxmox.window.Edit',
- alias: 'pbsTapeRestoreWindow',
+ extend: 'Ext.window.Window',
+ alias: 'widget.pbsTapeRestoreWindow',
mixins: ['Proxmox.Mixin.CBind'],
- width: 400,
- title: gettext('Restore Media Set'),
+ title: gettext('Restore Media-Set'),
+
+ width: 800,
+ height: 500,
+
url: '/api2/extjs/tape/restore',
method: 'POST',
- showTaskViewer: true,
- isCreate: true,
- defaults: {
- labelWidth: 120,
+ 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: 'displayfield',
- fieldLabel: gettext('Media Set'),
- cbind: {
- value: '{mediaset}',
- },
+ 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,
},
{
- xtype: 'displayfield',
- fieldLabel: gettext('Media Set UUID'),
- name: 'media-set',
- submitValue: true,
- cbind: {
- value: '{uuid}',
+ 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();
+ },
+ },
},
},
- {
- xtype: 'pbsDataStoreSelector',
- fieldLabel: gettext('Datastore'),
- name: 'store',
+ ],
+});
+
+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: [
{
- xtype: 'pbsDriveSelector',
- fieldLabel: gettext('Drive'),
- name: 'drive',
+ text: gettext('Source Datastore'),
+ dataIndex: 'store',
+ filter: {
+ type: 'list',
+ },
+ flex: 1,
},
{
- xtype: 'pbsUserSelector',
- name: 'notify-user',
- fieldLabel: gettext('Notify User'),
- emptyText: gettext('Current User'),
- value: null,
- allowBlank: true,
- renderer: Ext.String.htmlEncode,
+ 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,
+ },
+ ],
+ },
});