From 01ad47af9e3ff06f90bae6ca441e1f920fda59c3 Mon Sep 17 00:00:00 2001 From: Aaron Lauterer Date: Tue, 7 Jul 2020 11:49:00 +0200 Subject: [PATCH] gui: dc/backup: add new backup job detail view 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 --- 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 8295331a..bf9ceda9 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 ` ${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.39.2