[pve-devel] [PATCH manager 1/5 v2] Storage GUI: Rewrite backup content view as TreePanel.

Fabian Ebner f.ebner at proxmox.com
Tue Mar 22 09:42:58 CET 2022


Nit: for the commit title, prefixing like "ui: storage:" is preferred.

I really think we should start with the tree fully expanded, or we will
get shouted at by users relying on notes to find their backups ;)

@Thomas: The backups are now ordered ascending by date again. In PBS
it's also like that. Should that be changed or do we switch back in PVE?

Am 18.03.22 um 14:52 schrieb Matthias Heiserer:
> +Ext.define('PVE.storage.BackupView', {
> +    extend: 'Ext.tree.Panel',
>      alias: 'widget.pveStorageBackupView',
> +    mixins: ['Proxmox.Mixin.CBind'],
> +    rootVisible: false,
>  
> -    showColumns: ['name', 'notes', 'protected', 'date', 'format', 'size'],
> +    title: gettext('Content'),
>  
> -    initComponent: function() {
> -	var me = this;
> +    cbindData: function(initialCfg) {
> +	this.isPBS = initialCfg.pluginType === 'pbs';

That's part of what cbind should do for you if you return { isPBS: ...
}, and that's how it should be used to avoid breaking the abstraction.

But since the storage can change (in the guest backup view) this should
rather be a normal bind and be updated there.

> +	return {};
> +    },
> +    isStorage: false,

Doesn't seem to be used.

>  
> -	var nodename = me.nodename = me.pveSelNode.data.node;
> -	if (!nodename) {
> -	    throw "no node name specified";
> -	}
> +    controller: {
> +	xclass: 'Ext.app.ViewController',
>  
> -	var storage = me.storage = me.pveSelNode.data.storage;
> -	if (!storage) {
> -	    throw "no storage ID specified";
> -	}
> +	groupnameHelper: function(item) {

Nit: IMHO something like getGroupName() would be a bit more readable at
the call sites.

> +	    if (item.vmid) {
> +		return PVE.Utils.get_backup_type(item.volid, item.format) + `/${item.vmid}`;
> +	    } else {
> +		return 'Other';
> +	    }
> +	},
>  
> -	me.content = 'backup';
> +	init: function(view) {
> +	    let me = this;
> +	    me.storage = view?.pveSelNode?.data?.storage;

Nit: here you use ?., but below you assume it's set (which I suppose we
can).

> +	    me.nodename = view.nodename || view.pveSelNode.data.node;

Is there ever a case where it's not the selected node?

> +	    me.vmid = view.pveSelNode.data.vmid;
> +	    me.vmtype = view.pveSelNode.data.type;
>  
> -	var sm = me.sm = Ext.create('Ext.selection.RowModel', {});
> +	    me.store = Ext.create('Ext.data.Store', {
> +		model: 'PVE.storage.BackupModel',
> +	    });
> +	    me.store.on('load', me.onLoad, me);
> +	    view.getStore().setConfig('filterer', 'bottomup');
> +	    view.getStore().setSorters(['text']);
>  
> -	var reload = function() {
> -	    me.store.load();
> -	};
> +	    if (me.vmid) {
> +		me.getView().getStore().filter({
> +		    property: 'vmid',
> +		    value: me.vmid,
> +		    exactMatch: true,
> +		});
> +	    } else {
> +		me.lookup('storagesel').setVisible(false);
> +		me.lookup('backupNowButton').setVisible(false);
> +	    }
> +	    Proxmox.Utils.monStoreErrors(view, me.store);
> +	},
>  
> -	let pruneButton = Ext.create('Proxmox.button.Button', {
> -	    text: gettext('Prune group'),
> -	    disabled: true,
> -	    selModel: sm,
> -	    setBackupGroup: function(backup) {
> -		if (backup) {
> -		    let name = backup.text;
> -		    let vmid = backup.vmid;
> -		    let format = backup.format;
> +	onLoad: function(store, records, success, operation) {
> +	    let me = this;
> +	    let view = me.getView();
> +	    let selection = view.getSelection()?.[0];
> +	    selection = selection?.parentNode?.data?.text +selection?.data?.volid;

Style nit: missing space after + and could use `${...}${...}` syntax
instead.

(...)

> +	    if (selection) {
> +		let rootnode = view.getRootNode();
> +		let selected;
> +		rootnode.cascade(node => {
> +		    if (selected) {return false;} // skip if already found

Style nit: if body on the same line

> +		    let id = node.parentNode?.data?.text + node.data?.volid;
> +		    if (id === selection) {
> +			selected = node;
> +			return false;
> +		    }
> +		    return true;
>  		});
> -		win.show();
> -		win.on('destroy', reload);
> -	    },
> -	});
> +		view.setSelection(selected);
> +		view.getView().focusRow(selected);

After removing a backup I get a
  Uncaught TypeError: node is undefined
referencing this line.

> +	    }
> +	    Proxmox.Utils.setErrorMask(view, false);
> +	},
>  

(...)

> +
> +	removeHandler: function(button, event, rec) {
> +	    let me = this;
> +	    const volid = rec.data.volid;
> +	    Proxmox.Utils.API2Request({
> +		url: `/nodes/${me.nodename}/storage/${me.storage}/content//${volid}`,

Nit: duplicate / in line above

> +		method: 'DELETE',
> +		callback: () => me.reload(),
> +		failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
> +	    });
> +	},
> +
> +	searchKeyupFn: function(field) {
> +	    let me = this;
> +	    me.getView().getStore().getFilters().removeByKey('volid');
> +	    me.getView().getStore().filter([
> +		{
> +		    property: 'volid',
> +		    value: field.getValue(),
> +		    anyMatch: true,
> +		    caseSensitive: false,
> +		    id: 'volid',
>  		},
> -		verification: {
> -		    header: gettext('Verify State'),
> -		    dataIndex: 'verification',
> -		    renderer: PVE.Utils.render_backup_verification,
> +	    ]);
> +	},
> +
> +	searchClearHandler: function(field) {
> +	    field.triggers.clear.setVisible(false);
> +	    field.setValue(this.originalValue);
> +	    this.getView().getStore().clearFilter();
> +	},
> +
> +	searchChangeFn: function(field, newValue, oldValue) {
> +	    if (newValue !== field.originalValue) {
> +		field.triggers.clear.setVisible(true);
> +	    }
> +	},
> +
> +	storageSelectorBoxReady: function(selector, width, height, eOpts) {
> +	    selector.setNodename(this.nodename);
> +	},

Would cbind also be an option?

> +
> +	storageSelectorChange: function(self, newValue, oldValue, eOpts) {
> +	    let me = this;
> +	    me.storage = newValue;
> +	    me.getView().getSelectionModel().deselectAll();
> +	    me.reload();
> +	},
> +
> +	backupNowHandler: function(button, event) {
> +	    let me = this;
> +	    Ext.create('PVE.window.Backup', {
> +		nodename: me.nodename,
> +		vmid: me.vmid,
> +		vmtype: me.vmtype,
> +		storage: me.storage,
> +		listeners: {
> +		    close: () => me.reload(),
>  		},
> -	    };
> -	}
> +	    }).show();
> +	},
> +    },
> +
> +    columns: [
> +	{
> +	    xtype: 'treecolumn',
> +	    header: gettext("Backup Group"),
> +	    dataIndex: 'text',
> +	    renderer: function(value, _metadata, record) {
> +		if (record.phantom) { return value; }

Style nit: if body on the same line. Could be avoided using ternary ? here.

> +		return PVE.Utils.render_storage_content(...arguments);
> +	    },
> +	    flex: 2,
> +	},
> +	{
> +	    header: gettext('Notes'),
> +	    flex: 1,
> +	    renderer: Ext.htmlEncode,
> +	    dataIndex: 'notes',
> +	},
> +	{
> +	    header: `<i class="fa fa-shield"></i>`,
> +	    tooltip: gettext('Protected'),
> +	    width: 30,
> +	    renderer: v => v ? `<i data-qtip="${gettext('Protected')}" class="fa fa-shield"></i>` : '',

Style nit: line too long

> +	    sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0),
> +	    dataIndex: 'protected',
> +	},
> +	{
> +	    header: gettext('Date'),
> +	    width: 150,
> +	    dataIndex: 'ctime',
> +	    xtype: 'datecolumn',
> +	    format: 'Y-m-d H:i:s',
> +	},
> +	{
> +	    header: gettext('Format'),
> +	    width: 100,
> +	    dataIndex: 'format',
> +	},
> +	{
> +	    header: gettext('Size'),
> +	    width: 100,
> +	    renderer: Proxmox.Utils.format_size,
> +	    dataIndex: 'size',
> +	},
> +	{
> +	    header: gettext('Encrypted'),
> +	    dataIndex: 'encrypted',
> +	    renderer: PVE.Utils.render_backup_encryption,
> +	    cbind: {
> +		hidden: '{!isPBS}',
> +	    },
> +	},
> +	{
> +	    header: gettext('Verify State'),
> +	    dataIndex: 'verification',
> +	    renderer: PVE.Utils.render_backup_verification,
> +	    cbind: {
> +		hidden: '{!isPBS}',
> +	    },
> +	},
> +    ],
> +
> +    tbar: [
> +	{
> +	    xtype: 'button',
> +	    text: gettext('Backup now'),
> +	    handler: 'backupNowHandler',
> +	    reference: 'backupNowButton',
> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Restore'),
> +	    handler: 'restoreHandler',
> +	    parentXType: "treepanel",
> +	    disabled: true,
> +	    enableFn: record => record.phantom === false,

Style nit: !record.phantom is shorter

Repeated below a few times

> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('File Restore'),
> +	    handler: 'restoreFilesHandler',
> +	    cbind: {
> +		hidden: '{!isPBS}',
> +	    },
> +	    parentXType: "treepanel",
> +	    disabled: true,
> +	    enableFn: record => record.phantom === false,
> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Show Configuration'),
> +	    handler: 'showConfigurationHandler',
> +	    parentXType: "treepanel",
> +	    disabled: true,
> +	    enableFn: record => record.phantom === false,
> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Edit Notes'),
> +	    handler: 'editNotesHandler',
> +	    parentXType: "treepanel",
> +	    disabled: true,
> +	    enableFn: record => record.phantom === false,
> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Change Protection'),
> +	    handler: 'changeProtectionHandler',
> +	    parentXType: "treepanel",
> +	    disabled: true,
> +	    enableFn: record => record.phantom === false,
> +	},
> +	'-',
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Prune group'),
> +	    setBackupGroup: function(backup) {
> +		let me = this;
> +		if (backup) {
> +		    let volid = backup.volid;
> +		    let vmid = backup.vmid;
> +		    let format = backup.format;
>  
> -	me.callParent();
> +		    let vmtype = PVE.Utils.get_backup_type(volid, format);
> +		    if (vmid && vmtype) {
> +			me.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`);
> +			me.vmid = vmid;
> +			me.vmtype = vmtype;
> +			me.setDisabled(false);
> +			return;
> +		    }
> +		}
> +		me.setText(gettext('Prune group'));
> +		me.vmid = null;
> +		me.vmtype = null;
> +		me.setDisabled(true);
> +	    },
> +	    handler: 'pruneGroupHandler',
> +	    parentXType: "treepanel",
> +	    disabled: true,
> +	    reference: 'pruneButton',
> +	    enableFn: () => true,

This makes the disabling in setBackupGroup not work as intended.

> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Remove'),
> +	    handler: 'removeHandler',
> +	    parentXType: 'treepanel',
> +	    disabled: true,
> +	    enableFn: record => record.phantom === false && !record?.data?.protected,
> +	    confirmMsg: function(rec) {
> +		let name = rec.data.text;
> +		return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${name}'`);

Sytle nit: line too long

> +	    },
> +	},
> +	'->',
> +	{
> +	    xtype: 'pveStorageSelector',
> +	    fieldLabel: gettext('Storage'),
> +	    storageContent: 'backup',
> +	    reference: 'storagesel',
> +	    listeners: {
> +		change: 'storageSelectorChange',
> +		boxready: 'storageSelectorBoxReady',
> +	    },
>  
> -	me.store.getSorters().clear();
> -	me.store.setSorters([
> -	    {
> -		property: 'vmid',
> -		direction: 'ASC',
> +	},
> +	gettext('Search') + ':',
> +	' ',
> +	{
> +	    xtype: 'textfield',
> +	    width: 200,
> +	    enableKeyEvents: true,
> +	    emptyText: gettext('Name, Format'),
> +	    listeners: {
> +		keyup: {
> +		    buffer: 500,
> +		    fn: 'searchKeyupFn',
> +		},
> +		change: 'searchChangeFn',
>  	    },
> -	    {
> -		property: 'vdate',
> -		direction: 'DESC',
> +	    triggers: {
> +		clear: {
> +		    cls: 'pmx-clear-trigger',
> +		    weight: -1,
> +		    hidden: true,
> +		    handler: 'searchClearHandler',
> +		},
>  	    },
> -	]);
> +	},
> +	],
> +
> +    listeners: {
> +	activate: function() {
> +	    let me = this;
> +	    // only load on first activate to not load every tab switch
> +	    if (!me.firstLoad) {
> +		me.getController().reload();
> +		me.firstLoad = true;
> +	    }

This does not seem to apply here, because there are no tabs to switch
to/from. Switching to and back from a different storage content type
leads to me.firstLoad being undefined again. But I'd actually want to
see changes when doing so in any case ;)

> +	},
> +	selectionchange: function(model, srecords, eOpts) {
> +	    let pruneButton = this.getController().lookup('pruneButton');
> +	    if (srecords.length === 1) {
> +		pruneButton.setBackupGroup(srecords[0].data);
> +	    } else {
> +		pruneButton.setBackupGroup(null);
> +	    }
> +	},
>      },
>  });





More information about the pve-devel mailing list