pve-manager/www/manager6/tree/SnapshotTree.js
Thomas Lamprecht 9cc4958f5a ui snapshot tree: avoid exception in delayd load when view is gone
If one switched through guest fast (e.g., keeping the down-arrow key
pressed) while staying on the snapshot panel, it could happen that
the previous view got already destroyed once the success callback of
the feature API request got executed.

Then the ExtJS ViewModels' set method got a "null" back from its
me.getStub(...) call, and tried to access members of that, resulting
in a TypeError exception.

Avoid that by checking if we're already destroyed or still around
before doing that call. During the time we are already in the
callback we shouldn't be able to get destroyed in parallel due to JS
single thread nature and no yield point here, so this is safe.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-01-30 19:43:20 +01:00

367 lines
8.7 KiB
JavaScript

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: (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) => gettext(get('snapshotAllowed') ? 'Edit' : '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,
});
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) {
Proxmox.Utils.setErrorMask(view, response.htmlStatus);
me.load_task.delay(load_delay);
},
success: function(response, opts) {
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) {
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('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',
},
{
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);
}
}
}
],
});