mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-06-08 03:51:01 +00:00

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>
367 lines
8.7 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}
|
|
],
|
|
|
|
});
|