[pve-devel] [PATCH v4 manager 3/5] gui: dc/backup: add new backup job detail view

Aaron Lauterer a.lauterer at proxmox.com
Tue Jul 7 11:49:00 CEST 2020


The new detail view for backup jobs shows the settings similar to the
edit dialog but read only. Additionally it does show a list of all
included guests with their volumes and whether these volumes will be
included in the backup.

Signed-off-by: Aaron Lauterer <a.lauterer at proxmox.com>
---
v3 -> v4:
* added search box
* removed "not all permissions" notification as we don't show such a
  notifcation naywhere else. makes dealing with the API data esier
* fixed if clauses to show the correct selection mode. `exclude vmids`
  was overruled by `all` in previous version.
* added more returned reasons to the backup_reasons_table. The qemu side
  got new return values in the latest patches [0]


v2->v3:
* made the comment why we need to split the "vmid:key" ID disks
 more descriptive
* changed double negative for permissions `not_all_permissions` to
  `permissions_for_all`

v1->v2:
* made render_backup_status more generic
* reworked the reasons why a volume might not be included. turns out we
   cannot easily determine if a volume is in-/excluded by default or
   because the user actively wanted it

   [0] https://pve.proxmox.com/pipermail/pve-devel/2020-June/044052.html

 www/manager6/Utils.js     |  34 ++++
 www/manager6/dc/Backup.js | 372 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 405 insertions(+), 1 deletion(-)

diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index 158b370b..91e6238b 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -219,6 +219,31 @@ Ext.define('PVE.Utils', { utilities: {
 
     },
 
+    render_backup_status: function(value, meta, record) {
+	if (typeof value == 'undefined') {
+	    return "";
+	}
+
+	let iconCls = 'check-circle good';
+	let text = gettext('Yes');
+
+	if (!PVE.Parser.parseBoolean(value.toString())) {
+	    iconCls = 'times-circle critical';
+
+	    text = gettext('No');
+
+	    let reason = record.get('reason');
+	    if (typeof reason !== 'undefined') {
+		if (reason in PVE.Utils.backup_reasons_table) {
+		    reason = PVE.Utils.backup_reasons_table[record.get('reason')];
+		}
+		text = `${text} - ${reason}`;
+	    }
+	}
+
+	return `<i class="fa fa-${iconCls}"></i> ${text}`;
+    },
+
     render_backup_days_of_week: function(val) {
 	var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
 	var selected = [];
@@ -279,6 +304,15 @@ Ext.define('PVE.Utils', { utilities: {
 	return "-";
     },
 
+    backup_reasons_table: {
+	'backup=yes': gettext('Enabled'),
+	'backup=no': gettext('Disabled'),
+	'enabled': gettext('Enabled'),
+	'disabled': gettext('Disabled'),
+	'not a volume': gettext('Not a volume'),
+	'efidisk but no OMVF BIOS': gettext('EFI Disk without OMVF BIOS'),
+    },
+
     get_kvm_osinfo: function(value) {
 	var info = { base: 'Other' }; // default
 	if (value) {
diff --git a/www/manager6/dc/Backup.js b/www/manager6/dc/Backup.js
index 1ef092c5..1e070510 100644
--- a/www/manager6/dc/Backup.js
+++ b/www/manager6/dc/Backup.js
@@ -375,6 +375,327 @@ Ext.define('PVE.dc.BackupEdit', {
 });
 
 
+Ext.define('PVE.dc.BackupDiskTree', {
+    extend: 'Ext.tree.Panel',
+    alias: 'widget.pveBackupDiskTree',
+
+    folderSort: true,
+    rootVisible: false,
+
+    store: {
+	sorters: 'id',
+	data: {},
+    },
+
+    tools: [
+	{
+	    type: 'expand',
+	    tooltip: gettext('Expand All'),
+	    scope: this,
+	    callback: function(panel) {
+		panel.expandAll();
+	    },
+	},
+	{
+	    type: 'collapse',
+	    tooltip: gettext('Collapse All'),
+	    scope: this,
+	    callback: function(panel) {
+		panel.collapseAll();
+	    }
+	},
+    ],
+
+    columns: [
+	{
+	    xtype: 'treecolumn',
+	    text: gettext('Guest Image'),
+	    renderer: function(value, meta, record) {
+		if (record.data.type) {
+		    // guest level
+		    let ret = value;
+		    if (record.data.name) {
+			ret += " (" + record.data.name + ")";
+		    }
+		    return ret;
+		} else {
+		    // volume level
+		    // extJS needs unique IDs but we only want to show the
+		    // volumes key from "vmid:key"
+		    return value.split(':')[1] + " - " + record.data.name;
+		}
+	    },
+	    dataIndex: 'id',
+	    flex: 6,
+	},
+	{
+	    text: gettext('Type'),
+	    dataIndex: 'type',
+	    flex: 1,
+	},
+	{
+	    text: gettext('Backup Job'),
+	    renderer: PVE.Utils.render_backup_status,
+	    dataIndex: 'included',
+	    flex: 3,
+	},
+    ],
+
+    reload: function() {
+	var me = this;
+	var sm = me.getSelectionModel();
+
+	Proxmox.Utils.API2Request({
+	    url: "/cluster/backup/" + me.jobid + "/included_volumes",
+	    waitMsgTarget: me,
+	    method: 'GET',
+	    failure: function(response, opts) {
+		Proxmox.Utils.setErrorMask(me, response.htmlStatus);
+	    },
+	    success: function(response, opts) {
+		sm.deselectAll();
+		me.setRootNode(response.result.data);
+		me.expandAll();
+	    },
+	});
+    },
+
+    initComponent: function() {
+	var me = this;
+
+	if (!me.jobid) {
+	    throw "no job id specified";
+	}
+
+	var sm = Ext.create('Ext.selection.TreeModel', {});
+
+	Ext.apply(me, {
+	    selModel: sm,
+	    fields: ['id', 'type',
+		{
+		    type: 'string',
+		    name: 'iconCls',
+		    calculate: function(data) {
+			var txt = 'fa x-fa-tree fa-';
+			if (data.leaf && !data.type) {
+			    return txt + 'hdd-o';
+			} else if (data.type === 'qemu') {
+			    return txt + 'desktop';
+			} else if (data.type === 'lxc') {
+			    return txt + 'cube';
+			} else {
+			    return txt + 'question-circle';
+			}
+		    }
+		}
+	    ],
+	    tbar: [
+	        '->',
+		gettext('Search') + ':', ' ',
+		{
+		    xtype: 'textfield',
+		    width: 200,
+		    enableKeyEvents: true,
+		    listeners: {
+			buffer: 500,
+			keyup: function(field) {
+			    let searchValue = field.getValue();
+			    searchValue = searchValue.toLowerCase();
+
+			    me.store.clearFilter(true);
+			    me.store.filterBy(function(record) {
+				let match = false;
+
+				let data = '';
+				if (record.data.depth == 0) {
+				    return true;
+				} else if (record.data.depth == 1) {
+				    data = record.data;
+				} else if (record.data.depth == 2) {
+				    data = record.parentNode.data;
+				}
+
+				Ext.each(['name', 'id', 'type'], function(property) {
+				    if (data[property] == null) {
+					return;
+				    }
+
+				    let v = data[property].toString();
+				    if (v !== undefined) {
+					v = v.toLowerCase();
+					if (v.includes(searchValue)) {
+					    match = true;
+					    return;
+					}
+				    }
+				});
+				return match;
+			    });
+			}
+		    }
+		}
+	    ],
+	});
+
+	me.callParent();
+
+	me.reload();
+    }
+});
+
+Ext.define('PVE.dc.BackupInfo', {
+    extend: 'Proxmox.panel.InputPanel',
+    alias: 'widget.pveBackupInfo',
+
+    padding: 10,
+
+    column1: [
+	{
+	    name: 'node',
+	    fieldLabel: gettext('Node'),
+	    xtype: 'displayfield',
+	    renderer: function (value) {
+		if (!value) {
+		    return '-- ' + gettext('All') + ' --';
+		} else {
+		    return value;
+		}
+	    },
+	},
+	{
+	    name: 'storage',
+	    fieldLabel: gettext('Storage'),
+	    xtype: 'displayfield',
+	},
+	{
+	    name: 'dow',
+	    fieldLabel: gettext('Day of week'),
+	    xtype: 'displayfield',
+	    renderer: PVE.Utils.render_backup_days_of_week,
+	},
+	{
+	    name: 'starttime',
+	    fieldLabel: gettext('Start Time'),
+	    xtype: 'displayfield',
+	},
+	{
+	    name: 'selMode',
+	    fieldLabel: gettext('Selection mode'),
+	    xtype: 'displayfield',
+	},
+	{
+	    name: 'pool',
+	    fieldLabel: gettext('Pool to backup'),
+	    xtype: 'displayfield',
+	}
+    ],
+    column2: [
+	{
+	    name: 'mailto',
+	    fieldLabel: gettext('Send email to'),
+	    xtype: 'displayfield',
+	},
+	{
+	    name: 'mailnotification',
+	    fieldLabel: gettext('Email notification'),
+	    xtype: 'displayfield',
+	    renderer: function (value) {
+		let msg;
+		switch (value) {
+		    case 'always':
+			msg = gettext('Always');
+			break;
+		    case 'failure':
+			msg = gettext('On failure only');
+			break;
+		}
+		return msg;
+	    },
+	},
+	{
+	    name: 'compress',
+	    fieldLabel: gettext('Compression'),
+	    xtype: 'displayfield',
+	},
+	{
+	    name: 'mode',
+	    fieldLabel: gettext('Mode'),
+	    xtype: 'displayfield',
+	    renderer: function (value) {
+		let msg;
+		switch (value) {
+		    case 'snapshot':
+			msg = gettext('Snapshot');
+			break;
+		    case 'suspend':
+			msg = gettext('Suspend');
+			break;
+		    case 'stop':
+			msg = gettext('Stop');
+			break;
+		}
+		return msg;
+	    },
+	},
+	{
+	    name: 'enabled',
+	    fieldLabel: gettext('Enabled'),
+	    xtype: 'displayfield',
+	    renderer: function (value) {
+		if (PVE.Parser.parseBoolean(value.toString())) {
+		    return gettext('Yes');
+		} else {
+		    return gettext('No');
+		}
+	    },
+	},
+    ],
+
+    setValues: function(values) {
+	var me = this;
+
+        Ext.iterate(values, function(fieldId, val) {
+	    let field = me.query('[isFormField][name=' + fieldId + ']')[0];
+	    if (field) {
+		field.setValue(val);
+            }
+	});
+
+	// selection Mode depends on the presence/absence of several keys
+	let selModeField = me.query('[isFormField][name=selMode]')[0];
+	let selMode = 'none';
+	if (values.vmid) {
+	    selMode = gettext('Include selected VMs');
+	}
+	if (values.all) {
+	    selMode = gettext('All');
+	}
+	if (values.exclude) {
+	     selMode = gettext('Exclude selected VMs');
+	}
+	if (values.pool) {
+	    selMode = gettext('Pool based');
+	}
+	selModeField.setValue(selMode);
+
+	if (!values.pool) {
+	    let poolField = me.query('[isFormField][name=pool]')[0];
+	    poolField.setVisible(0);
+	}
+    },
+
+    initComponent: function() {
+	var me = this;
+
+	if (!me.record) {
+	    throw "no data provided";
+	}
+	me.callParent();
+
+	me.setValues(me.record);
+    }
+});
+
 Ext.define('PVE.dc.BackupView', {
     extend: 'Ext.grid.GridPanel',
 
@@ -414,6 +735,45 @@ Ext.define('PVE.dc.BackupView', {
 	    win.show();
 	};
 
+	var run_detail = function() {
+	    let record = sm.getSelection()[0]
+	    if (!record) {
+		return;
+	    }
+	    var me = this;
+	    var infoview = Ext.create('PVE.dc.BackupInfo', {
+		flex: 0,
+		layout: 'fit',
+		record: record.data,
+	    });
+	    var disktree = Ext.create('PVE.dc.BackupDiskTree', {
+		title: gettext('Included disks'),
+		flex: 1,
+		jobid: record.data.id,
+	    });
+
+	    var win = Ext.create('Ext.window.Window', {
+		modal: true,
+		width: 600,
+		height: 500,
+		stateful: true,
+		stateId: 'backup-detail-view',
+		resizable: true,
+		layout: 'fit',
+		title: gettext('Backup Details'),
+
+		items:[{
+		    xtype: 'panel',
+		    region: 'center',
+		    layout: {
+			type: 'vbox',
+			align: 'stretch'
+		    },
+		    items: [infoview, disktree],
+		}]
+	    }).show();
+	};
+
 	var run_backup_now = function(job) {
 	    job = Ext.clone(job);
 
@@ -514,6 +874,14 @@ Ext.define('PVE.dc.BackupView', {
 	    }
 	});
 
+	var detail_btn = new Proxmox.button.Button({
+	    text: gettext('Detail'),
+	    disabled: true,
+	    tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'),
+	    selModel: sm,
+	    handler: run_detail,
+	});
+
 	Proxmox.Utils.monStoreErrors(me, store);
 
 	Ext.apply(me, {
@@ -536,8 +904,10 @@ Ext.define('PVE.dc.BackupView', {
 		'-',
 		remove_btn,
 		edit_btn,
+		detail_btn,
+		'-',
+		run_btn,
 		'-',
-		run_btn
 	    ],
 	    columns: [
 		{
-- 
2.20.1






More information about the pve-devel mailing list