all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH v5 manager] ui: storage backup view: add prune window
@ 2020-11-24 13:00 Fabian Ebner
  2020-11-24 13:28 ` [pve-devel] applied: " Thomas Lamprecht
  0 siblings, 1 reply; 2+ messages in thread
From: Fabian Ebner @ 2020-11-24 13:00 UTC (permalink / raw)
  To: pve-devel

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





^ permalink raw reply	[flat|nested] 2+ messages in thread

* [pve-devel] applied: [PATCH v5 manager] ui: storage backup view: add prune window
  2020-11-24 13:00 [pve-devel] [PATCH v5 manager] ui: storage backup view: add prune window Fabian Ebner
@ 2020-11-24 13:28 ` Thomas Lamprecht
  0 siblings, 0 replies; 2+ messages in thread
From: Thomas Lamprecht @ 2020-11-24 13:28 UTC (permalink / raw)
  To: Proxmox VE development discussion, Fabian Ebner

On 24.11.20 14:00, Fabian Ebner wrote:
> 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
> 
>

applied, thanks!




^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2020-11-24 13:29 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2020-11-24 13:00 [pve-devel] [PATCH v5 manager] ui: storage backup view: add prune window Fabian Ebner
2020-11-24 13:28 ` [pve-devel] applied: " Thomas Lamprecht

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