[pve-devel] [PATCH manager 2/3] gui: refator SnapshotTree

Thomas Lamprecht t.lamprecht at proxmox.com
Thu Jan 30 20:08:51 CET 2020


On 1/30/20 4:58 PM, Dominik Csapak wrote:
> using the better View, ViewModel, Controller style,
> while doing this, make it generic so that we can use it for qemu and lxc
> 
> Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
> ---
>  www/manager6/Makefile             |   3 +-
>  www/manager6/lxc/Config.js        |   3 +-
>  www/manager6/lxc/SnapshotTree.js  | 330 ---------------------------
>  www/manager6/qemu/Config.js       |   3 +-
>  www/manager6/qemu/SnapshotTree.js | 320 --------------------------
>  www/manager6/tree/SnapshotTree.js | 361 ++++++++++++++++++++++++++++++
>  6 files changed, 366 insertions(+), 654 deletions(-)
>  delete mode 100644 www/manager6/lxc/SnapshotTree.js
>  delete mode 100644 www/manager6/qemu/SnapshotTree.js
>  create mode 100644 www/manager6/tree/SnapshotTree.js
> 
> [snip]

> diff --git a/www/manager6/tree/SnapshotTree.js b/www/manager6/tree/SnapshotTree.js
> new file mode 100644
> index 00000000..d4007efa
> --- /dev/null
> +++ b/www/manager6/tree/SnapshotTree.js
> @@ -0,0 +1,361 @@
> +Ext.define('PVE.guest.SnapshotTree', {
> +    extend: 'Ext.tree.Panel',
> +    xtype: 'pveGuestSnapshotTree',
> +
> +    stateful: true,
> +    stateId: 'grid-snapshots',
> +
> +    viewModel: {
> +	data: {
> +	    // should be 'qemu' or 'lxc'
> +	    type: undefined,
> +	    nodename: undefined,
> +	    vmid: undefined,
> +	    snapshotAllowed: false,
> +	    rollbackAllowed: false,
> +	    snapshotFeature: false,
> +	    selected: '',
> +	    load_delay: 3000,
> +	},
> +	formulas: {
> +	    canSnapshot: function(get) {
> +		return get('snapshotAllowed') && get('snapshotFeature');
> +	    },
> +	    canRollback: function(get) {
> +		return get('rollbackAllowed')  &&
> +		    get('selected') && get('selected') !== 'current';
> +	    },
> +	    canRemove: function(get) {
> +		return get('snapshotAllowed') &&
> +		    get('selected') && get('selected') !== 'current';
> +	    },
> +	    isSnapshot: function(get) {
> +		return get('selected') && get('selected') !== 'current';
> +	    },
> +	    buttonText: function(get) {
> +		return get('snapshotAllowed') ? gettext('Edit') : gettext('View');
> +	    },
> +	    showMemory: function(get) {
> +		return get('type') === 'qemu';
> +	    },

opnionated style fix for above, use arror functions and reused isSnapshot formula
in other formulas.

> +	},
> +    },
> +
> +    controller: {
> +	xclass: 'Ext.app.ViewController',
> +
> +	newSnapshot: function() {
> +	    this.run_editor(false);
> +	},
> +
> +	editSnapshot: function() {
> +	    this.run_editor(true);
> +	},
> +
> +	run_editor: function(edit) {
> +	    let me = this;
> +	    let vm = me.getViewModel();
> +	    let snapname;
> +	    if (edit) {
> +		snapname = vm.get('selected');
> +		if (!snapname || snapname === 'current') { return; }
> +	    }
> +	    let win = Ext.create('PVE.window.Snapshot', {
> +		nodename: vm.get('nodename'),
> +		vmid: vm.get('vmid'),
> +		viewonly: !vm.get('snapshotAllowed'),
> +		type: vm.get('type'),
> +		isCreate: !edit,
> +		submitText: !edit ? gettext('Take Snapshot') : undefined,
> +		snapname: snapname,
> +	    });
> +	    win.show();
> +	    me.mon(win, 'destroy', me.reload, me);
> +	},
> +
> +	snapshotAction: function(action, method) {
> +	    let me = this;
> +	    let view = me.getView();
> +	    let vm = me.getViewModel();
> +	    let snapname = vm.get('selected');
> +	    if (!snapname) { return; }
> +
> +	    let nodename = vm.get('nodename');
> +	    let type = vm.get('type');
> +	    let vmid = vm.get('vmid');
> +
> +	    Proxmox.Utils.API2Request({
> +		url: `/nodes/${nodename}/${type}/${vmid}/snapshot/${snapname}/${action}`,
> +		method: method,
> +		waitMsgTarget: view,
> +		callback: function() {
> +		    me.reload();
> +		},
> +		failure: function (response, opts) {
> +		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
> +		},
> +		success: function(response, options) {
> +		    var upid = response.result.data;
> +		    var win = Ext.create('Proxmox.window.TaskProgress', { upid: upid });
> +		    win.show();
> +		}
> +	    });
> +	},
> +
> +	rollback: function() { this.snapshotAction('rollback', 'POST'); },
> +	remove: function() { this.snapshotAction('', 'DELETE'); },

followed up for above crammed in one line stuff, looks especially weird because
cancel below has also just a one-liner method-body, but is formatted nicely.

> +
> +	cancel: function() {
> +	    this.load_task.cancel();
> +	},
> +
> +	reload: function() {
> +	    let me = this;
> +	    let view = me.getView();
> +	    let vm = me.getViewModel();
> +	    let nodename = vm.get('nodename');
> +	    let vmid = vm.get('vmid');
> +	    let type = vm.get('type');
> +	    let load_delay = vm.get('load_delay');
> +
> +	    Proxmox.Utils.API2Request({
> +		url: `/nodes/${nodename}/${type}/${vmid}/snapshot`,
> +		method: 'GET',
> +		failure: function(response, opts) {
added me.destroyed guard here (see below)

> +		    Proxmox.Utils.setErrorMask(view, response.htmlStatus);
> +		    me.load_task.delay(load_delay);
> +		},
> +		success: function(response, opts) {

added me.destroyed guard here (see below)

> +		    Proxmox.Utils.setErrorMask(view, false);
> +		    var digest = 'invalid';
> +		    var idhash = {};
> +		    var root = { name: '__root', expanded: true, children: [] };
> +		    Ext.Array.each(response.result.data, function(item) {
> +			item.leaf = true;
> +			item.children = [];
> +			if (item.name === 'current') {
> +			    digest = item.digest + item.running;
> +			    item.iconCls = PVE.Utils.get_object_icon_class(vm.get('type'), item);
> +			} else {
> +			    item.iconCls = 'fa fa-fw fa-history x-fa-tree';
> +			}
> +			idhash[item.name] = item;
> +		    });
> +
> +		    if (digest !== me.old_digest) {
> +			me.old_digest = digest;
> +
> +			Ext.Array.each(response.result.data, function(item) {
> +			    if (item.parent && idhash[item.parent]) {
> +				var parent_item = idhash[item.parent];
> +				parent_item.children.push(item);
> +				parent_item.leaf = false;
> +				parent_item.expanded = true;
> +				parent_item.expandable = false;
> +			    } else {
> +				root.children.push(item);
> +			    }
> +			});
> +
> +			me.getView().setRootNode(root);
> +		    }
> +
> +		    me.load_task.delay(load_delay);
> +		}
> +	    });
> +
> +	    // if we do not have the permissions, we don't have to check
> +	    // if we can create a snapshot, since the butten stays disabled
> +	    if (!vm.get('snapshotAllowed')) {
> +		return;
> +	    }
> +
> +	    Proxmox.Utils.API2Request({
> +		url: `/nodes/${nodename}/${type}/${vmid}/feature`,
> +		params: { feature: 'snapshot' },
> +		method: 'GET',
> +		success: function(response, options) {
> +		    var res = response.result.data; vm.set('snapshotFeature', !!res.hasFeature); }

fixed above "mess", sorry but the mix of two statement + closing bracket for
the success callback on the same line really got me wondering what's going on ^^


Also, put a guard against me.destroyed here, which could happen if this callback
got, well, called back once the component was destroyed already - e.g., when the
user navigated to another guest/panel.

Was the simplest thing working I came up with quickly, better ideas are welcomed.

> +	    });
> +	},
> +
> +	select: function(grid, val) {
> +	    let vm = this.getViewModel();
> +	    if (val.length < 1) {
> +		vm.set('selected', '');
> +		return;
> +	    }
> +	    vm.set('selected', val[0].data.name);
> +	},
> +
> +	init: function(view) {
> +	    let me = this;
> +	    let vm = me.getViewModel();
> +	    me.load_task = new Ext.util.DelayedTask(me.reload, me);
> +
> +	    if (!view.type) {
> +		throw 'guest type not set';
> +	    }
> +	    vm.set('type', view.type);
> +
> +	    if (!view.pveSelNode.data.node) {
> +		throw "no node name specified";
> +	    }
> +	    vm.set('nodename', view.pveSelNode.data.node);
> +
> +	    if (!view.pveSelNode.data.vmid) {
> +		throw "no VM ID specified";
> +	    }
> +	    vm.set('vmid', view.pveSelNode.data.vmid);
> +
> +	    let caps = Ext.state.Manager.get('GuiCap');
> +	    vm.set('snapshotAllowed', !!caps.vms['VM.Snapshot']);
> +	    vm.set('rollbackAllowed', !!caps.vms['VM.Snapshot.Rollback']);
> +
> +	    view.getStore().sorters.add({
> +		property: 'order',
> +		direction: 'ASC',
> +	    });
> +
> +	    me.reload();
> +	},
> +    },
> +
> +    listeners: {
> +	selectionchange: 'select',
> +	itemdblclick: 'editSnapshot',
> +	destroy: 'cancel',
> +    },
> +
> +    layout: 'fit',
> +    rootVisible: false,
> +    animate: false,
> +    sortableColumns: false,
> +
> +    tbar: [
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Take Snapshot'),
> +	    disabled: true,
> +	    bind: {
> +		disabled: "{!canSnapshot}",
> +	    },
> +	    handler: 'newSnapshot',
> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Rollback'),
> +	    disabled: true,
> +	    bind: {
> +		disabled: '{!canRollback}',
> +	    },
> +	    confirmMsg: function() {
> +		let view = this.up('treepanel');
> +		let rec = view.getSelection()[0];
> +		let vmid = view.getViewModel().get('vmid');
> +		return Proxmox.Utils.format_task_description('qmrollback', vmid) +
> +		    " '" +  rec.data.name + "'";
> +	    },
> +	    handler: 'rollback',
> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Remove'),
> +	    disabled: true,
> +	    bind: {
> +		disabled: '{!canRemove}',
> +	    },
> +	    confirmMsg: function() {
> +		let view = this.up('treepanel');
> +		let rec = view.getSelection()[0];
> +		return Ext.String.format(
> +		    gettext('Are you sure you want to remove entry {0}'),
> +		    `'${rec.data.name}'`
> +		);
> +	    },
> +	    handler: 'remove',
> +	},
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Edit'),
> +	    bind: {
> +		text: '{buttonText}',
> +		disabled: '{!isSnapshot}',
> +	    },
> +	    disabled: true,
> +	    edit: true,
> +	    handler: 'editSnapshot',
> +	}
> +    ],
> +
> +    columnLines: true,
> +
> +    fields: [
> +	'name', 'description', 'snapstate', 'vmstate', 'running',
> +	{ name: 'snaptime', type: 'date', dateFormat: 'timestamp' },

just saw above now, but it's actually something we have here and there, not biggest
fan but OK.

> +	{
> +	    name: 'order',
> +	    calculate: function(data) {
> +		return data.snaptime || (data.name === 'current' ? 'ZZZ' : data.snapstate);
> +	    }
> +	}
> +    ],
> +
> +    columns: [
> +	{
> +	    xtype: 'treecolumn',
> +	    text: gettext('Name'),
> +	    dataIndex: 'name',
> +	    width: 200,
> +	    renderer: function(value, metaData, record) {
> +		if (value === 'current') {
> +		    return gettext('NOW');
> +		} else {
> +		    return value;
> +		}
> +	    }
> +	},
> +	{
> +	    text: gettext('RAM'),
> +	    hidden: true,
> +	    bind: {
> +		hidden: '{!showMemory}',
> +	    },
> +	    align: 'center',
> +	    resizable: false,
> +	    dataIndex: 'vmstate',
> +	    width: 50,
> +	    renderer: function(value, metaData, record) {
> +		if (record.data.name !== 'current') {
> +		    return Proxmox.Utils.format_boolean(value);
> +		}
> +	    }
> +	},
> +	{
> +	    text: gettext('Date') + "/" + gettext("Status"),
> +	    dataIndex: 'snaptime',
> +	    width: 150,
> +	    renderer: function(value, metaData, record) {
> +		if (record.data.snapstate) {
> +		    return record.data.snapstate;
> +		}
> +		if (value) {
> +		    return Ext.Date.format(value,'Y-m-d H:i:s');
> +		}
> +	    }
> +	},
> +	{
> +	    text: gettext('Description'),
> +	    dataIndex: 'description',
> +	    flex: 1,
> +	    renderer: function(value, metaData, record) {
> +		if (record.data.name === 'current') {
> +		    return gettext("You are here!");
> +		} else {
> +		    return Ext.String.htmlEncode(value);
> +		}
> +	    }
> +	}
> +    ],
> +
> +});
> 




More information about the pve-devel mailing list