tape: ui: TapeRestore: make datastore mapping selectable
by adding a custom field (grid) where the user can select a target datastore for each source datastore on tape if we have not loaded the content of the media set yet, we have to load it on window open to get the list of datastores on the tape Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
This commit is contained in:
		
				
					committed by
					
						
						Dietmar Maurer
					
				
			
			
				
	
			
			
			
						parent
						
							a32bb86df9
						
					
				
				
					commit
					657c47db35
				
			@ -24,11 +24,18 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
 | 
			
		||||
		return;
 | 
			
		||||
	    }
 | 
			
		||||
 | 
			
		||||
	    let mediaset = selection[0].data.text;
 | 
			
		||||
	    let uuid = selection[0].data['media-set-uuid'];
 | 
			
		||||
	    let node = selection[0];
 | 
			
		||||
	    let mediaset = node.data.text;
 | 
			
		||||
	    let uuid = node.data['media-set-uuid'];
 | 
			
		||||
	    let datastores = node.data.datastores;
 | 
			
		||||
	    while (!datastores && node.get('depth') > 2) {
 | 
			
		||||
		node = node.parentNode;
 | 
			
		||||
		datastores = node.data.datastores;
 | 
			
		||||
	    }
 | 
			
		||||
	    Ext.create('PBS.TapeManagement.TapeRestoreWindow', {
 | 
			
		||||
		mediaset,
 | 
			
		||||
		uuid,
 | 
			
		||||
		datastores,
 | 
			
		||||
		listeners: {
 | 
			
		||||
		    destroy: function() {
 | 
			
		||||
			me.reload();
 | 
			
		||||
@ -185,6 +192,7 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		let storeList = Object.values(stores);
 | 
			
		||||
		let storeNameList = Object.keys(stores);
 | 
			
		||||
		let expand = storeList.length === 1;
 | 
			
		||||
		for (const store of storeList) {
 | 
			
		||||
		    store.children = Object.values(store.tapes);
 | 
			
		||||
@ -198,6 +206,7 @@ Ext.define('PBS.TapeManagement.BackupOverview', {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		node.set('loaded', true);
 | 
			
		||||
		node.set('datastores', storeNameList);
 | 
			
		||||
		Proxmox.Utils.setErrorMask(view, false);
 | 
			
		||||
		node.expand();
 | 
			
		||||
	    } catch (error) {
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
 | 
			
		||||
    extend: 'Proxmox.window.Edit',
 | 
			
		||||
    alias: 'pbsTapeRestoreWindow',
 | 
			
		||||
    alias: 'widget.pbsTapeRestoreWindow',
 | 
			
		||||
    mixins: ['Proxmox.Mixin.CBind'],
 | 
			
		||||
 | 
			
		||||
    width: 400,
 | 
			
		||||
    width: 800,
 | 
			
		||||
    title: gettext('Restore Media Set'),
 | 
			
		||||
    url: '/api2/extjs/tape/restore',
 | 
			
		||||
    method: 'POST',
 | 
			
		||||
@ -14,52 +14,272 @@ Ext.define('PBS.TapeManagement.TapeRestoreWindow', {
 | 
			
		||||
	labelWidth: 120,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    referenceHolder: true,
 | 
			
		||||
 | 
			
		||||
    items: [
 | 
			
		||||
	{
 | 
			
		||||
	    xtype: 'displayfield',
 | 
			
		||||
	    fieldLabel: gettext('Media Set'),
 | 
			
		||||
	    cbind: {
 | 
			
		||||
		value: '{mediaset}',
 | 
			
		||||
	    xtype: 'inputpanel',
 | 
			
		||||
 | 
			
		||||
	    onGetValues: function(values) {
 | 
			
		||||
		let me = this;
 | 
			
		||||
		let datastores = [];
 | 
			
		||||
		if (values.store && values.store !== "") {
 | 
			
		||||
		    datastores.push(values.store);
 | 
			
		||||
		    delete values.store;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (values.mapping) {
 | 
			
		||||
		    datastores.push(values.mapping);
 | 
			
		||||
		    delete values.mapping;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		values.store = datastores.join(',');
 | 
			
		||||
 | 
			
		||||
		return values;
 | 
			
		||||
	    },
 | 
			
		||||
 | 
			
		||||
	    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: 'pbsDriveSelector',
 | 
			
		||||
		    fieldLabel: gettext('Drive'),
 | 
			
		||||
		    name: 'drive',
 | 
			
		||||
		},
 | 
			
		||||
	    ],
 | 
			
		||||
 | 
			
		||||
	    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,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
		    xtype: 'pbsDataStoreSelector',
 | 
			
		||||
		    fieldLabel: gettext('Datastore'),
 | 
			
		||||
		    reference: 'defaultDatastore',
 | 
			
		||||
		    name: 'store',
 | 
			
		||||
		    listeners: {
 | 
			
		||||
			change: function(field, value) {
 | 
			
		||||
			    let me = this;
 | 
			
		||||
			    let grid = me.up('window').lookup('mappingGrid');
 | 
			
		||||
			    grid.setNeedStores(!value);
 | 
			
		||||
			},
 | 
			
		||||
		    },
 | 
			
		||||
		},
 | 
			
		||||
	    ],
 | 
			
		||||
 | 
			
		||||
	    columnB: [
 | 
			
		||||
		{
 | 
			
		||||
		    fieldLabel: gettext('Datastore Mapping'),
 | 
			
		||||
		    labelWidth: 200,
 | 
			
		||||
		    hidden: true,
 | 
			
		||||
		    reference: 'mappingLabel',
 | 
			
		||||
		    xtype: 'displayfield',
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
		    xtype: 'pbsDataStoreMappingField',
 | 
			
		||||
		    reference: 'mappingGrid',
 | 
			
		||||
		    name: 'mapping',
 | 
			
		||||
		    defaultBindProperty: 'value',
 | 
			
		||||
		    hidden: true,
 | 
			
		||||
		},
 | 
			
		||||
	    ],
 | 
			
		||||
	},
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    setDataStores: function(datastores) {
 | 
			
		||||
	let me = this;
 | 
			
		||||
 | 
			
		||||
	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('Datastore'));
 | 
			
		||||
	    defaultField.setAllowBlank(false);
 | 
			
		||||
	    defaultField.setEmptyText("");
 | 
			
		||||
	    return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	label.setVisible(true);
 | 
			
		||||
	defaultField.setFieldLabel(gettext('Default Datastore'));
 | 
			
		||||
	defaultField.setAllowBlank(true);
 | 
			
		||||
	defaultField.setEmptyText(Proxmox.Utils.NoneText);
 | 
			
		||||
 | 
			
		||||
	grid.setDataStores(datastores);
 | 
			
		||||
	grid.setVisible(true);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    initComponent: function() {
 | 
			
		||||
	let me = this;
 | 
			
		||||
 | 
			
		||||
	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;
 | 
			
		||||
			}
 | 
			
		||||
			me.setDataStores(Object.keys(datastores));
 | 
			
		||||
		    },
 | 
			
		||||
		    failure: function() {
 | 
			
		||||
			// ignore failing api call, maybe catalog is missing
 | 
			
		||||
			me.setDataStores();
 | 
			
		||||
		    },
 | 
			
		||||
		});
 | 
			
		||||
	    }, 10);
 | 
			
		||||
	}
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
Ext.define('PBS.TapeManagement.DataStoreMappingGrid', {
 | 
			
		||||
    extend: 'Ext.grid.Panel',
 | 
			
		||||
    alias: 'widget.pbsDataStoreMappingField',
 | 
			
		||||
    mixins: ['Ext.form.field.Field'],
 | 
			
		||||
 | 
			
		||||
    getValue: function() {
 | 
			
		||||
	let me = this;
 | 
			
		||||
	let datastores = [];
 | 
			
		||||
	me.getStore().each((rec) => {
 | 
			
		||||
	    let source = rec.data.source;
 | 
			
		||||
	    let target = rec.data.target;
 | 
			
		||||
	    if (target && target !== "") {
 | 
			
		||||
		datastores.push(`${source}=${target}`);
 | 
			
		||||
	    }
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	return datastores.join(',');
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    // this determines if we need at least one valid mapping
 | 
			
		||||
    needStores: false,
 | 
			
		||||
 | 
			
		||||
    setNeedStores: function(needStores) {
 | 
			
		||||
	let me = this;
 | 
			
		||||
	me.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.needStores) {
 | 
			
		||||
	    error = true;
 | 
			
		||||
	    me.getStore().each((rec) => {
 | 
			
		||||
		if (rec.data.target) {
 | 
			
		||||
		    error = false;
 | 
			
		||||
		}
 | 
			
		||||
	    });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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);
 | 
			
		||||
 | 
			
		||||
	    return [errorMsg];
 | 
			
		||||
	}
 | 
			
		||||
	me.removeCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
 | 
			
		||||
	me.getActionEl().dom.setAttribute('data-errorqtip', "");
 | 
			
		||||
	return [];
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    setDataStores: function(datastores) {
 | 
			
		||||
	let me = this;
 | 
			
		||||
	let store = me.getStore();
 | 
			
		||||
	let data = [];
 | 
			
		||||
 | 
			
		||||
	for (const datastore of datastores) {
 | 
			
		||||
	    data.push({
 | 
			
		||||
		source: datastore,
 | 
			
		||||
		target: '',
 | 
			
		||||
	    });
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	store.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',
 | 
			
		||||
		allowBlank: true,
 | 
			
		||||
		emptyText: Proxmox.Utils.NoneText,
 | 
			
		||||
		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',
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
	    xtype: 'pbsDriveSelector',
 | 
			
		||||
	    fieldLabel: gettext('Drive'),
 | 
			
		||||
	    name: 'drive',
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
	    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,
 | 
			
		||||
	},
 | 
			
		||||
    ],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user