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, running: false, selected: '', load_delay: 3000, }, formulas: { canSnapshot: (get) => get('snapshotAllowed') && get('snapshotFeature'), canRollback: (get) => get('rollbackAllowed') && get('isSnapshot'), canRemove: (get) => get('snapshotAllowed') && get('isSnapshot'), isSnapshot: (get) => get('selected') && get('selected') !== 'current', buttonText: (get) => get('snapshotAllowed') ? gettext('Edit') : gettext('View'), showMemory: (get) => get('type') === 'qemu', }, }, 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, running: vm.get('running'), }); 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'); }, 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) { if (me.destroyed) return; Proxmox.Utils.setErrorMask(view, response.htmlStatus); me.load_task.delay(load_delay); }, success: function(response, opts) { if (me.destroyed) { // this is in a delayed task, avoid dragons if view has // been destroyed already and go home. return; } 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') { vm.set('running', !!item.running); 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) { if (me.destroyed) { // this is in a delayed task, the current view could been // destroyed already; then we mustn't do viemodel set return; } let res = response.result.data; vm.set('snapshotFeature', !!res.hasFeature); }, }); }, 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('Edit'), bind: { text: '{buttonText}', disabled: '{!isSnapshot}', }, disabled: true, edit: true, handler: 'editSnapshot', }, { xtype: 'proxmoxButton', text: gettext('Remove'), disabled: true, dangerous: 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: 'label', text: gettext("The current guest configuration does not support taking new snapshots"), hidden: true, bind: { hidden: "{canSnapshot}", }, }, ], columnLines: true, fields: [ 'name', 'description', 'snapstate', 'vmstate', 'running', { name: 'snaptime', type: 'date', dateFormat: 'timestamp' }, { 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); } }, }, ], });