public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Fabian Ebner <f.ebner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v3 manager 20/20] backup view: add prune window
Date: Fri, 13 Nov 2020 14:16:33 +0100	[thread overview]
Message-ID: <20201113131633.21915-21-f.ebner@proxmox.com> (raw)
In-Reply-To: <20201113131633.21915-1-f.ebner@proxmox.com>

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@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





  parent reply	other threads:[~2020-11-13 13:17 UTC|newest]

Thread overview: 22+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-11-13 13:16 [pve-devel] [PATCH-SERIES v3 manager] split up content view into a view for each type Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 01/20] fix extension filter for upload window Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 02/20] config panel: allow new nodes to be added later Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 03/20] storage panel/browser: use insertNodes function Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 04/20] add CD ROM and lxc icons for treelist-item-icon Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 05/20] use separate view for each content type Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 06/20] remove the now unnecessary grouping by " Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 07/20] remove the now unneccessary content type column Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 08/20] content view: allow specifying title bar elements for init Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 09/20] turn {nodename, storage, sm} into object variables Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 10/20] add upload button conditionally Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 11/20] create and use TemplateView Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 12/20] create and use BackupView Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 13/20] get rid of unneccessary enableFn's Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 14/20] create ImageView and use it for VM and CT images Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 15/20] simplify reload call Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [RFC v3 manager 16/20] content view: allow specifying which columns to show on init Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 17/20] group backups by backup groups in backup view Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 18/20] allow child classes of ContentView to specify their own listeners Fabian Ebner
2020-11-13 13:16 ` [pve-devel] [PATCH v3 manager 19/20] add CSS for button in grid's group header Fabian Ebner
2020-11-13 13:16 ` Fabian Ebner [this message]
2020-11-16  9:11 ` [pve-devel] patrially-applied-series: [PATCH-SERIES v3 manager] split up content view into a view for each type Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20201113131633.21915-21-f.ebner@proxmox.com \
    --to=f.ebner@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal