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 v4 manager 4/4] ui: storage backup view: add prune window
Date: Wed, 18 Nov 2020 11:04:20 +0100	[thread overview]
Message-ID: <20201118100420.21167-5-f.ebner@proxmox.com> (raw)
In-Reply-To: <20201118100420.21167-1-f.ebner@proxmox.com>

adapted from PBS. Main differences are:
    * loading of the prune-backups options from the storage configuration if
      configured
    * 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>
---

Changes from v3:
    * don't use grouping headers with a prune button. Instead use a button in
      the toolbar which displays the group to be pruned
    * always use 'lxc' and 'qemu' as backup types (which the PVE API expects) instead
      of 'VM' and 'CT' as types, to avoid some conversion

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

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 4fa8e1a3..b95bd9a2 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..6598d0f8
--- /dev/null
+++ b/www/manager6/window/Prune.js
@@ -0,0 +1,300 @@
+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_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





  parent reply	other threads:[~2020-11-18 10:04 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2020-11-18 10:04 [pve-devel] [PATCH-SERIES v4 manager] split up content view/add " Fabian Ebner
2020-11-18 10:04 ` [pve-devel] [PATCH v4 manager 1/4] cluster resources: include content type for storages Fabian Ebner
2020-11-18 10:04 ` [pve-devel] [PATCH v4 manager 2/4] ui: storage: get content types from resources Fabian Ebner
2020-11-18 10:04 ` [pve-devel] [PATCH v4 manager 3/4] ui: make remaining content views not stateful Fabian Ebner
2020-11-18 10:04 ` Fabian Ebner [this message]
2020-11-23  6:08   ` [pve-devel] [PATCH v4 manager 4/4] ui: storage backup view: add prune window Thomas Lamprecht
2020-11-23  6:09 ` [pve-devel] partially-applied: [PATCH-SERIES v4 manager] split up content view/add " 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=20201118100420.21167-5-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