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.csa...@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); > + } > + } > + } > + ], > + > +}); > _______________________________________________ pve-devel mailing list pve-devel@pve.proxmox.com https://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel