* [pve-devel] [PATCH v4 manager 2/4] ui: storage: get content types from resources
2020-11-18 10:04 [pve-devel] [PATCH-SERIES v4 manager] split up content view/add prune window 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 ` Fabian Ebner
2020-11-18 10:04 ` [pve-devel] [PATCH v4 manager 3/4] ui: make remaining content views not stateful Fabian Ebner
` (2 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Fabian Ebner @ 2020-11-18 10:04 UTC (permalink / raw)
To: pve-devel
to avoid waiting for a status API call.
Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
---
New in v4
Commit ed5d5403508a4713fd8a6c720ceaf11562352415 (introducing the insertNodes
function) could be reverted after this.
www/manager6/storage/Browser.js | 144 +++++++++++++++-----------------
1 file changed, 68 insertions(+), 76 deletions(-)
diff --git a/www/manager6/storage/Browser.js b/www/manager6/storage/Browser.js
index 81387111..64c492d8 100644
--- a/www/manager6/storage/Browser.js
+++ b/www/manager6/storage/Browser.js
@@ -38,89 +38,81 @@ Ext.define('PVE.storage.Browser', {
hstateid: 'storagetab'
});
- // call here, so there is a root for insertNodes()
- me.callParent();
-
if (caps.storage['Datastore.Allocate'] ||
caps.storage['Datastore.AllocateSpace'] ||
caps.storage['Datastore.Audit']) {
+ let storageInfo = PVE.data.ResourceStore.findRecord(
+ 'id',
+ `storage/${nodename}/${storeid}`,
+ );
+ let contents = storageInfo.data.content.split(',');
- Proxmox.Utils.API2Request({
- url: `/nodes/${nodename}/storage/${storeid}/status`,
- method: 'GET',
- success: function(response, opts) {
- var contents = response.result.data.content.split(',');
- var items = [];
-
- if (contents.includes('backup')) {
- items.push({
- xtype: 'pveStorageBackupView',
- title: gettext('Backups'),
- iconCls: 'fa fa-floppy-o',
- itemId: 'contentBackup',
- hasCommentColumn: true,
- });
- }
- if (contents.includes('images')) {
- items.push({
- xtype: 'pveStorageImageView',
- title: gettext('VM Disks'),
- iconCls: 'fa fa-hdd-o',
- itemId: 'contentImages',
- content: 'images',
- });
- }
- if (contents.includes('rootdir')) {
- items.push({
- xtype: 'pveStorageImageView',
- title: gettext('CT Volumes'),
- iconCls: 'fa fa-hdd-o lxc',
- itemId: 'contentRootdir',
- content: 'rootdir',
- });
- }
- if (contents.includes('iso')) {
- items.push({
- xtype: 'pveStorageContentView',
- title: gettext('ISO Images'),
- iconCls: 'pve-itype-treelist-item-icon-cdrom',
- itemId: 'contentIso',
- content: 'iso',
- useUploadButton: true,
- });
- }
- if (contents.includes('vztmpl')) {
- items.push({
- xtype: 'pveStorageTemplateView',
- title: gettext('CT Templates'),
- iconCls: 'fa fa-file-o lxc',
- itemId: 'contentVztmpl',
- });
- }
- if (contents.includes('snippets')) {
- items.push({
- xtype: 'pveStorageContentView',
- title: gettext('Snippets'),
- iconCls: 'fa fa-file-code-o',
- itemId: 'contentSnippets',
- content: 'snippets',
- });
- }
- me.insertNodes(items);
- },
- });
+ if (contents.includes('backup')) {
+ me.items.push({
+ xtype: 'pveStorageBackupView',
+ title: gettext('Backups'),
+ iconCls: 'fa fa-floppy-o',
+ itemId: 'contentBackup',
+ hasCommentColumn: true,
+ });
+ }
+ if (contents.includes('images')) {
+ me.items.push({
+ xtype: 'pveStorageImageView',
+ title: gettext('VM Disks'),
+ iconCls: 'fa fa-hdd-o',
+ itemId: 'contentImages',
+ content: 'images',
+ });
+ }
+ if (contents.includes('rootdir')) {
+ me.items.push({
+ xtype: 'pveStorageImageView',
+ title: gettext('CT Volumes'),
+ iconCls: 'fa fa-hdd-o lxc',
+ itemId: 'contentRootdir',
+ content: 'rootdir',
+ });
+ }
+ if (contents.includes('iso')) {
+ me.items.push({
+ xtype: 'pveStorageContentView',
+ title: gettext('ISO Images'),
+ iconCls: 'pve-itype-treelist-item-icon-cdrom',
+ itemId: 'contentIso',
+ content: 'iso',
+ useUploadButton: true,
+ });
+ }
+ if (contents.includes('vztmpl')) {
+ me.items.push({
+ xtype: 'pveStorageTemplateView',
+ title: gettext('CT Templates'),
+ iconCls: 'fa fa-file-o lxc',
+ itemId: 'contentVztmpl',
+ });
+ }
+ if (contents.includes('snippets')) {
+ me.items.push({
+ xtype: 'pveStorageContentView',
+ title: gettext('Snippets'),
+ iconCls: 'fa fa-file-code-o',
+ itemId: 'contentSnippets',
+ content: 'snippets',
+ });
+ }
}
if (caps.storage['Permissions.Modify']) {
- me.insertNodes([
- {
- xtype: 'pveACLView',
- title: gettext('Permissions'),
- iconCls: 'fa fa-unlock',
- itemId: 'permissions',
- path: '/storage/' + storeid
- },
- ]);
+ me.items.push({
+ xtype: 'pveACLView',
+ title: gettext('Permissions'),
+ iconCls: 'fa fa-unlock',
+ itemId: 'permissions',
+ path: '/storage/' + storeid
+ });
}
+
+ me.callParent();
}
});
--
2.20.1
^ permalink raw reply [flat|nested] 7+ messages in thread
* [pve-devel] [PATCH v4 manager 4/4] ui: storage backup view: add prune window
2020-11-18 10:04 [pve-devel] [PATCH-SERIES v4 manager] split up content view/add prune window Fabian Ebner
` (2 preceding siblings ...)
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
2020-11-23 6:08 ` Thomas Lamprecht
2020-11-23 6:09 ` [pve-devel] partially-applied: [PATCH-SERIES v4 manager] split up content view/add " Thomas Lamprecht
4 siblings, 1 reply; 7+ messages in thread
From: Fabian Ebner @ 2020-11-18 10:04 UTC (permalink / raw)
To: pve-devel
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
^ permalink raw reply [flat|nested] 7+ messages in thread