[pve-devel] [PATCH v3 manager 20/20] backup view: add prune window

Fabian Ebner f.ebner at proxmox.com
Fri Nov 13 14:16:33 CET 2020


adapted from PBS. Main differences are:
    * loading of the prune-backups options from the storage configuration
    * API has GET/DELETE distinction instead of 'dry-run'
    * API expects a single property string for the prune options

Also, had to change the clear trigger, because now there can be original
values (from the storage config), but it doesn't really make sense to
reset to that value when clearing, so always set to 'null' when clearing

Signed-off-by: Fabian Ebner <f.ebner at proxmox.com>
---

Depends on this patch (otherwise it would error
out when all numberfields are cleared):
https://lists.proxmox.com/pipermail/pve-devel/2020-November/045916.html

 www/manager6/Makefile              |   1 +
 www/manager6/storage/BackupView.js |  34 +++-
 www/manager6/window/Prune.js       | 306 +++++++++++++++++++++++++++++
 3 files changed, 340 insertions(+), 1 deletion(-)
 create mode 100644 www/manager6/window/Prune.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 6111a9ce..a23b3a1e 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -97,6 +97,7 @@ JSSRC= 							\
 	window/LoginWindow.js				\
 	window/Migrate.js				\
 	window/NotesEdit.js				\
+	window/Prune.js					\
 	window/Restore.js				\
 	window/SafeDestroy.js				\
 	window/Settings.js				\
diff --git a/www/manager6/storage/BackupView.js b/www/manager6/storage/BackupView.js
index 9ced7947..9ee14399 100644
--- a/www/manager6/storage/BackupView.js
+++ b/www/manager6/storage/BackupView.js
@@ -6,7 +6,19 @@ Ext.define('PVE.storage.BackupView', {
     features: [
 	{
 	    ftype: 'grouping',
-	    groupHeaderTpl: '{name} ({rows.length} Backup{[values.rows.length > 1 ? "s" : ""]})',
+	    groupHeaderTpl: new Ext.XTemplate(
+		'<tpl for=".">',
+		'{name} ({rows.length} Backup{[values.rows.length > 1 ? "s" : ""]})',
+		'<tpl if="name != \'other\'">',
+		' | ' + gettext('Prune') + ': ',
+		'<button id="prune-btn" ' +
+		    'class="x-btn x-unselectable x-btn-custom-groupheader" ' +
+		    'data-qtip="Prune">',
+		'<i id="prune-btn-icon" class="fa fa-scissors"></i>',
+		'</button>',
+		'</tpl>',
+		'</tpl>',
+	    ),
 	},
     ],
 
@@ -91,6 +103,26 @@ Ext.define('PVE.storage.BackupView', {
 	    },
 	});
 
+	Ext.apply(me, {
+	    listeners: {
+		groupclick: function (view, node, group, e, eOpts) {
+		    if (e.getTarget().id.startsWith('prune-btn')) {
+			view.features[0].expand(group); // keep group to be pruned expanded
+
+			let [ type, vmid ] = group.split('/');
+			var win = Ext.create('PVE.window.Prune', {
+			    nodename: nodename,
+			    storage: storage,
+			    backup_id: vmid,
+			    backup_type: type,
+			});
+			win.show();
+			win.on('destroy', reload);
+		    }
+		},
+	    },
+	});
+
 	me.callParent();
     },
 });
diff --git a/www/manager6/window/Prune.js b/www/manager6/window/Prune.js
new file mode 100644
index 00000000..f211dc91
--- /dev/null
+++ b/www/manager6/window/Prune.js
@@ -0,0 +1,306 @@
+Ext.define('pve-prune-list', {
+    extend: 'Ext.data.Model',
+    fields: [
+	'type',
+	'vmid',
+	{
+	    name: 'ctime',
+	    type: 'date',
+	    dateFormat: 'timestamp',
+	},
+    ],
+});
+
+Ext.define('PVE.PruneKeepInput', {
+    extend: 'Proxmox.form.field.Integer',
+    alias: 'widget.pvePruneKeepInput',
+
+    allowBlank: true,
+    minValue: 1,
+
+    listeners: {
+	change: function(field, newValue, oldValue) {
+	    if (newValue === 0) { // might be configured in the storage options
+		this.setValue(null);
+		this.triggers.clear.setVisible(false);
+	    } else {
+		this.triggers.clear.setVisible(newValue !== null);
+	    }
+	},
+    },
+    triggers: {
+	clear: {
+	    cls: 'pmx-clear-trigger',
+	    weight: -1,
+	    hidden: true,
+	    handler: function() {
+		this.triggers.clear.setVisible(false);
+		this.setValue(null);
+	    },
+	},
+    },
+});
+
+Ext.define('PVE.PruneInputPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    alias: 'widget.pvePruneInputPanel',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onGetValues: function(values) {
+	let me = this;
+
+	// the API expects a single prune-backups property string
+	let pruneBackups = PVE.Parser.printPropertyString(values);
+	values = {
+	    'prune-backups': pruneBackups,
+	    'type': me.backup_type,
+	    'vmid': me.backup_id,
+	};
+
+	return values;
+    },
+
+    controller: {
+	xclass: 'Ext.app.ViewController',
+
+	init: function(view) {
+	    if (!view.url) {
+		throw "no url specified";
+	    }
+	    if (!view.backup_type) {
+		throw "no backup_type specified";
+	    }
+	    if (!view.backup_id) {
+		throw "no backup_id specified";
+	    }
+
+	    this.reload(); // initial load
+	},
+
+	reload: function() {
+	    let view = this.getView();
+
+	    // helper to allow showing why a backup is kept
+	    let addKeepReasons = function(backups, params) {
+		const rules = [
+		    'keep-last',
+		    'keep-hourly',
+		    'keep-daily',
+		    'keep-weekly',
+		    'keep-monthly',
+		    'keep-yearly',
+		    'keep-all', // when all keep options are not set
+		];
+		let counter = {};
+
+		backups.sort(function(a, b) {
+		    return a.ctime < b.ctime;
+		});
+
+		let ruleIndex = -1;
+		let nextRule = function() {
+		    let rule;
+		    do {
+			ruleIndex++;
+			rule = rules[ruleIndex];
+		    } while (!params[rule] && rule !== 'keep-all');
+		    counter[rule] = 0;
+		    return rule;
+		};
+
+		let rule = nextRule();
+		for (let backup of backups) {
+		    if (backup.mark === 'keep') {
+			counter[rule]++;
+			if (rule !== 'keep-all') {
+			    backup.keepReason = rule + ': ' + counter[rule];
+			    if (counter[rule] >= params[rule]) {
+				rule = nextRule();
+			    }
+			} else {
+			    backup.keepReason = rule;
+			}
+		    }
+		}
+	    };
+
+	    let params = view.getValues();
+	    let keepParams = PVE.Parser.parsePropertyString(params["prune-backups"]);
+
+	    Proxmox.Utils.API2Request({
+		url: view.url,
+		method: "GET",
+		params: params,
+		callback: function() {
+		    // for easy breakpoint setting
+		},
+		failure: function(response, opts) {
+		    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+		},
+		success: function(response, options) {
+		    var data = response.result.data;
+		    addKeepReasons(data, keepParams);
+		    view.pruneStore.setData(data);
+		},
+	    });
+	},
+
+	control: {
+	    field: { change: 'reload' },
+	},
+    },
+
+    column1: [
+	{
+	    xtype: 'pvePruneKeepInput',
+	    name: 'keep-last',
+	    fieldLabel: gettext('keep-last'),
+	},
+	{
+	    xtype: 'pvePruneKeepInput',
+	    name: 'keep-hourly',
+	    fieldLabel: gettext('keep-hourly'),
+	},
+	{
+	    xtype: 'pvePruneKeepInput',
+	    name: 'keep-daily',
+	    fieldLabel: gettext('keep-daily'),
+	},
+	{
+	    xtype: 'pvePruneKeepInput',
+	    name: 'keep-weekly',
+	    fieldLabel: gettext('keep-weekly'),
+	},
+	{
+	    xtype: 'pvePruneKeepInput',
+	    name: 'keep-monthly',
+	    fieldLabel: gettext('keep-monthly'),
+	},
+	{
+	    xtype: 'pvePruneKeepInput',
+	    name: 'keep-yearly',
+	    fieldLabel: gettext('keep-yearly'),
+	},
+    ],
+
+    initComponent: function() {
+        var me = this;
+
+	me.pruneStore = Ext.create('Ext.data.Store', {
+	    model: 'pve-prune-list',
+	    sorters: { property: 'ctime', direction: 'DESC' },
+	});
+
+	Proxmox.Utils.API2Request({
+	    url: "/storage",
+	    method: 'GET',
+	    success: function(response, opts) {
+		let scfg = response.result.data.find(x => x.storage === me.storage);
+		if (!scfg || !scfg["prune-backups"]) {
+		    return;
+		}
+		let prune_opts = PVE.Parser.parsePropertyString(scfg["prune-backups"]);
+		me.setValues(prune_opts);
+	    },
+	});
+
+	me.column2 = [
+	    {
+		xtype: 'grid',
+		height: 200,
+		store: me.pruneStore,
+		columns: [
+		    {
+			header: gettext('Backup Time'),
+			sortable: true,
+			dataIndex: 'ctime',
+			renderer: function(value, metaData, record) {
+			    let text = Ext.Date.format(value, 'Y-m-d H:i:s');
+			    if (record.data.mark === 'remove') {
+				return '<div style="text-decoration: line-through;">'+ text +'</div>';
+			    } else {
+				return text;
+			    }
+			},
+			flex: 1,
+		    },
+		    {
+			text: 'Keep (reason)',
+			dataIndex: 'mark',
+			renderer: function(value, metaData, record) {
+			    if (record.data.mark === 'keep') {
+				return 'true (' + record.data.keepReason + ')';
+			    } else if (record.data.mark === 'protected') {
+				return 'true (strange name)';
+			    } else {
+				return 'false';
+			    }
+			},
+			flex: 1,
+		    },
+		],
+	    },
+	];
+
+	me.callParent();
+    },
+});
+
+Ext.define('PVE.window.Prune', {
+    extend: 'Proxmox.window.Edit',
+
+    method: 'DELETE',
+    submitText: gettext("Prune"),
+
+    fieldDefaults: { labelWidth: 130 },
+
+    isCreate: true,
+
+    initComponent: function() {
+        var me = this;
+
+	if (!me.nodename) {
+	    throw "no nodename specified";
+	}
+	if (!me.storage) {
+	    throw "no storage specified";
+	}
+	if (!me.backup_type) {
+	    throw "no backup_type specified";
+	}
+	if (!me.backup_id) {
+	    throw "no backup_id specified";
+	}
+
+	let backupGroupStr = me.backup_type + '/' + me.backup_id;
+
+	if (me.backup_type === 'CT') {
+	    me.backup_type = 'lxc';
+	} else if (me.backup_type === 'VM') {
+	    me.backup_type = 'qemu';
+	} else {
+	    throw "unknown backup type";
+	}
+	let title = Ext.String.format(
+	    gettext("Prune Backups for '{0}' on Storage '{1}'"),
+	    backupGroupStr,
+	    me.storage,
+	);
+
+	Ext.apply(me, {
+	    url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups",
+	    title: title,
+	    items: [
+		{
+		    xtype: 'pvePruneInputPanel',
+		    url: '/api2/extjs/nodes/' + me.nodename + '/storage/' + me.storage + "/prunebackups",
+		    backup_type: me.backup_type,
+		    backup_id: me.backup_id,
+		    storage: me.storage,
+		},
+	    ],
+	});
+
+	me.callParent();
+    },
+});
-- 
2.20.1






More information about the pve-devel mailing list