all lists on 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 v5 manager] ui: storage backup view: add prune window
Date: Tue, 24 Nov 2020 14:00:53 +0100	[thread overview]
Message-ID: <20201124130053.10494-1-f.ebner@proxmox.com> (raw)

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

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

Needs a dependency bump for proxmox-widget-toolkit.

Changes from v4:
    * Switch to widget toolkit's prune keep fields.
    * Don't load values from the storage initially. While it could be done,
      doing a manual prune doesn't need to have to do anything at all with the
      configuration on the storage. Problem is also that a clear trigger
      would reset to that value instead of clearing, which obviously makes
      sense when editing the storage configuration, but not really for
      doing a one-shot prune operation.
    * Use "renamed" as a reason instead of "strange name" for renamed backups

 www/manager6/Makefile              |   1 +
 www/manager6/storage/BackupView.js |  51 ++++++
 www/manager6/window/Prune.js       | 257 +++++++++++++++++++++++++++++
 3 files changed, 309 insertions(+)
 create mode 100644 www/manager6/window/Prune.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 9e6e56ef..85f90ecd 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 8c1e2ed6..632a1d36 100644
--- a/www/manager6/storage/BackupView.js
+++ b/www/manager6/storage/BackupView.js
@@ -24,6 +24,56 @@ Ext.define('PVE.storage.BackupView', {
 	    me.store.load();
 	};
 
+	let pruneButton = Ext.create('Proxmox.button.Button', {
+	    text: gettext('Prune group'),
+	    disabled: true,
+	    selModel: sm,
+	    setBackupGroup: function(backup) {
+		if (backup) {
+		    let name = backup.text;
+		    let vmid = backup.vmid;
+		    let format = backup.format;
+
+		    let vmtype;
+		    if (name.startsWith('vzdump-lxc-') || format === "pbs-ct") {
+			vmtype = 'lxc';
+		    } else if (name.startsWith('vzdump-qemu-') || format === "pbs-vm") {
+			vmtype = 'qemu';
+		    }
+
+		    if (vmid && vmtype) {
+			this.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`);
+			this.vmid = vmid;
+			this.vmtype = vmtype;
+			this.setDisabled(false);
+			return;
+		    }
+		}
+		this.setText(gettext('Prune group'));
+		this.vmid = null;
+		this.vmtype = null;
+		this.setDisabled(true);
+	    },
+	    handler: function(b, e, rec) {
+		let win = Ext.create('PVE.window.Prune', {
+		    nodename: nodename,
+		    storage: storage,
+		    backup_id: this.vmid,
+		    backup_type: this.vmtype,
+		});
+		win.show();
+		win.on('destroy', reload);
+	    },
+	});
+
+	me.on('selectionchange', function(model, srecords, eOpts) {
+	    if (srecords.length === 1) {
+		pruneButton.setBackupGroup(srecords[0].data);
+	    } else {
+		pruneButton.setBackupGroup(null);
+	    }
+	});
+
 	me.tbar = [
 	    {
 		xtype: 'proxmoxButton',
@@ -64,6 +114,7 @@ Ext.define('PVE.storage.BackupView', {
 		    win.show();
 		}
 	    },
+	    pruneButton,
 	];
 
 	me.callParent();
diff --git a/www/manager6/window/Prune.js b/www/manager6/window/Prune.js
new file mode 100644
index 00000000..f503773d
--- /dev/null
+++ b/www/manager6/window/Prune.js
@@ -0,0 +1,257 @@
+Ext.define('pve-prune-list', {
+    extend: 'Ext.data.Model',
+    fields: [
+	'type',
+	'vmid',
+	{
+	    name: 'ctime',
+	    type: 'date',
+	    dateFormat: 'timestamp',
+	},
+    ],
+});
+
+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: 'pmxPruneKeepField',
+	    name: 'keep-last',
+	    fieldLabel: gettext('keep-last'),
+	},
+	{
+	    xtype: 'pmxPruneKeepField',
+	    name: 'keep-hourly',
+	    fieldLabel: gettext('keep-hourly'),
+	},
+	{
+	    xtype: 'pmxPruneKeepField',
+	    name: 'keep-daily',
+	    fieldLabel: gettext('keep-daily'),
+	},
+	{
+	    xtype: 'pmxPruneKeepField',
+	    name: 'keep-weekly',
+	    fieldLabel: gettext('keep-weekly'),
+	},
+	{
+	    xtype: 'pmxPruneKeepField',
+	    name: 'keep-monthly',
+	    fieldLabel: gettext('keep-monthly'),
+	},
+	{
+	    xtype: 'pmxPruneKeepField',
+	    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' },
+	});
+
+	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 (renamed)';
+			    } 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_type !== 'qemu' && me.backup_type !== 'lxc') {
+	    throw "unknown backup type: " + me.backup_type;
+	}
+	if (!me.backup_id) {
+	    throw "no backup_id specified";
+	}
+
+	let title = Ext.String.format(
+	    gettext("Prune Backups for '{0}' on Storage '{1}'"),
+	    me.backup_type + '/' + me.backup_id,
+	    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





             reply	other threads:[~2020-11-24 13:00 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-11-24 13:00 Fabian Ebner [this message]
2020-11-24 13:28 ` [pve-devel] applied: " 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=20201124130053.10494-1-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal