Should be easier to read/use than the current flat list.
Backups are grouped by ID and type, so in case there are backups
with ID 100 for both CT and VM, this would create two separate
groups in the UI.
Date and size of group are taken from the latest backup.
Notes, Protection, Encrypted, and Verify State stay as default
value empty, empty, No, and None, respectively.

Code adapted from the existing backup view and the pbs
datastore content, where appropriate.

Signed-off-by: Matthias Heiserer <m.heise...@proxmox.com>
---
Changes from v1: 

-add BackupNow button to create backups when not used for storage

-Display combined size of backups for groups, except for PBS where
deduplication happens

-remove timeout for API call

-inform window/restore if storage is PBS

-use get_backup_type helper

-some bug fixes, e.g. text was used instead of volid

-remove groupField, doesn't do anything for TreeStore

-only sort by text, ie. `(qemu|lxc)/<VMID>`

-use render_storage_content helper for displaying backup group

-remove content from store as it's never used. everything is
 content-type backup because we get the data from API
 
-group by backuptype rather than format, so that e.g tar and tar.zst
 are in the same group.
 
-code cleanup: reorder statements, remove one-character variable,
 remove debug log, rename this to me, change filtering, rename
 notPBS to isPBS
 
-add text field to model. fix date field in model

-remember expanded nodes and selection

-rename backup model

-use filterer only with correct store

 
 www/manager6/storage/BackupView.js | 702 +++++++++++++++++++++--------
 1 file changed, 511 insertions(+), 191 deletions(-)

diff --git a/www/manager6/storage/BackupView.js 
b/www/manager6/storage/BackupView.js
index 2328c0fc..cbb1624b 100644
--- a/www/manager6/storage/BackupView.js
+++ b/www/manager6/storage/BackupView.js
@@ -1,223 +1,543 @@
-Ext.define('PVE.storage.BackupView', {
-    extend: 'PVE.storage.ContentView',
+Ext.define('PVE.storage.BackupModel', {
+    extend: 'Ext.data.Model',
+    fields: [
+       {
+           name: 'ctime',
+           type: 'date',
+           dateFormat: 'timestamp',
+       },
+       'format',
+       'volid',
+       'vmid',
+       'size',
+       'protected',
+       'notes',
+       'text',
+    ],
+});
 
+
+Ext.define('PVE.storage.BackupView', {
+    extend: 'Ext.tree.Panel',
     alias: 'widget.pveStorageBackupView',
+    mixins: ['Proxmox.Mixin.CBind'],
+    rootVisible: false,
 
-    showColumns: ['name', 'notes', 'protected', 'date', 'format', 'size'],
+    title: gettext('Content'),
 
-    initComponent: function() {
-       var me = this;
+    cbindData: function(initialCfg) {
+       this.isPBS = initialCfg.pluginType === 'pbs';
+       return {};
+    },
+    isStorage: false,
 
-       var nodename = me.nodename = me.pveSelNode.data.node;
-       if (!nodename) {
-           throw "no node name specified";
-       }
+    controller: {
+       xclass: 'Ext.app.ViewController',
 
-       var storage = me.storage = me.pveSelNode.data.storage;
-       if (!storage) {
-           throw "no storage ID specified";
-       }
+       groupnameHelper: function(item) {
+           if (item.vmid) {
+               return PVE.Utils.get_backup_type(item.volid, item.format) + 
`/${item.vmid}`;
+           } else {
+               return 'Other';
+           }
+       },
 
-       me.content = 'backup';
+       init: function(view) {
+           let me = this;
+           me.storage = view?.pveSelNode?.data?.storage;
+           me.nodename = view.nodename || view.pveSelNode.data.node;
+           me.vmid = view.pveSelNode.data.vmid;
+           me.vmtype = view.pveSelNode.data.type;
 
-       var sm = me.sm = Ext.create('Ext.selection.RowModel', {});
+           me.store = Ext.create('Ext.data.Store', {
+               model: 'PVE.storage.BackupModel',
+           });
+           me.store.on('load', me.onLoad, me);
+           view.getStore().setConfig('filterer', 'bottomup');
+           view.getStore().setSorters(['text']);
 
-       var reload = function() {
-           me.store.load();
-       };
+           if (me.vmid) {
+               me.getView().getStore().filter({
+                   property: 'vmid',
+                   value: me.vmid,
+                   exactMatch: true,
+               });
+           } else {
+               me.lookup('storagesel').setVisible(false);
+               me.lookup('backupNowButton').setVisible(false);
+           }
+           Proxmox.Utils.monStoreErrors(view, me.store);
+       },
 
-       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;
+       onLoad: function(store, records, success, operation) {
+           let me = this;
+           let view = me.getView();
+           let selection = view.getSelection()?.[0];
+           selection = selection?.parentNode?.data?.text 
+selection?.data?.volid;
 
-                   let vmtype;
-                   if (name.startsWith('vzdump-lxc-') || format === "pbs-ct") {
-                       vmtype = 'lxc';
-                   } else if (name.startsWith('vzdump-qemu-') || format === 
"pbs-vm") {
-                       vmtype = 'qemu';
+           let expanded = {};
+           view.getRootNode().cascadeBy({
+               before: item => {
+                   if (item.isExpanded() && !item.data.leaf) {
+                       let id = item.data.text;
+                       expanded[id] = true;
+                       return true;
                    }
+                   return false;
+               },
+               after: Ext.emptyFn,
+           });
+           let groups = me.getRecordGroups(records, expanded);
 
-                   if (vmid && vmtype) {
-                       this.setText(gettext('Prune group') + ` 
${vmtype}/${vmid}`);
-                       this.vmid = vmid;
-                       this.vmtype = vmtype;
-                       this.setDisabled(false);
-                       return;
-                   }
+           for (const item of records.map(i => i.data)) {
+               item.text = item.volid;
+               item.leaf = true;
+               item.iconCls = 'fa-file-o';
+               groups[me.groupnameHelper(item)].children.push(item);
+               groups[me.groupnameHelper(item)].size += item.size;
+           }
+
+           for (let [_name, group] of Object.entries(groups)) {
+               let children = group.children;
+               let latest = children.reduce((l, r) => l.ctime > r.ctime ? l : 
r);
+               group.ctime = latest.ctime;
+               if (view.isPBS) {
+                   group.size = latest.size;
                }
-               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,
+               let num_verified = children.reduce((l, r) => l + r.verification 
=== 'ok', 0);
+               group.verified = num_verified / children.length;
+           }
+
+           let children = [];
+           Object.entries(groups).forEach(e => children.push(e[1]));
+           view.setRootNode({
+               expanded: true,
+               children: children,
+           });
+
+           if (selection) {
+               let rootnode = view.getRootNode();
+               let selected;
+               rootnode.cascade(node => {
+                   if (selected) {return false;} // skip if already found
+                   let id = node.parentNode?.data?.text + node.data?.volid;
+                   if (id === selection) {
+                       selected = node;
+                       return false;
+                   }
+                   return true;
                });
-               win.show();
-               win.on('destroy', reload);
-           },
-       });
+               view.setSelection(selected);
+               view.getView().focusRow(selected);
+           }
+           Proxmox.Utils.setErrorMask(view, false);
+       },
 
-       me.on('selectionchange', function(model, srecords, eOpts) {
-           if (srecords.length === 1) {
-               pruneButton.setBackupGroup(srecords[0].data);
-           } else {
-               pruneButton.setBackupGroup(null);
+       reload: function() {
+           let me = this;
+           let view = me.getView();
+           if (!view.store || !me.store) {
+               console.warn('cannot reload, no store(s)');
+               return;
            }
-       });
-
-       let isPBS = me.pluginType === 'pbs';
-
-       me.tbar = [
-           {
-               xtype: 'proxmoxButton',
-               text: gettext('Restore'),
-               selModel: sm,
-               disabled: true,
-               handler: function(b, e, rec) {
-                   var vmtype;
-                   if (PVE.Utils.volume_is_qemu_backup(rec.data.volid, 
rec.data.format)) {
-                       vmtype = 'qemu';
-                   } else if (PVE.Utils.volume_is_lxc_backup(rec.data.volid, 
rec.data.format)) {
-                       vmtype = 'lxc';
-                   } else {
-                       return;
-                   }
 
-                   var win = Ext.create('PVE.window.Restore', {
-                       nodename: nodename,
-                       volid: rec.data.volid,
-                       volidText: 
PVE.Utils.render_storage_content(rec.data.volid, {}, rec),
-                       vmtype: vmtype,
-                       isPBS: isPBS,
-                   });
-                   win.show();
-                   win.on('destroy', reload);
-               },
-           },
-       ];
-       if (isPBS) {
-           me.tbar.push({
-               xtype: 'proxmoxButton',
-               text: gettext('File Restore'),
-               disabled: true,
-               selModel: sm,
-               handler: function(b, e, rec) {
-                   let isVMArchive = 
PVE.Utils.volume_is_qemu_backup(rec.data.volid, rec.data.format);
-                   Ext.create('Proxmox.window.FileBrowser', {
-                       title: gettext('File Restore') + " - " + rec.data.text,
-                       listURL: 
`/api2/json/nodes/localhost/storage/${me.storage}/file-restore/list`,
-                       downloadURL: 
`/api2/json/nodes/localhost/storage/${me.storage}/file-restore/download`,
-                       extraParams: {
-                           volume: rec.data.volid,
-                       },
-                       archive: isVMArchive ? 'all' : undefined,
-                       autoShow: true,
-                   });
+           if (!me.storage) {
+               Proxmox.Utils.setErrorMask(view, true);
+               return;
+           }
+
+           let url = 
`/api2/json/nodes/${me.nodename}/storage/${me.storage}/content`;
+           me.store.setProxy({
+               type: 'proxmox',
+               url: url,
+               extraParams: {
+                   content: 'backup',
                },
            });
-       }
-       me.tbar.push(
-           {
-               xtype: 'proxmoxButton',
-               text: gettext('Show Configuration'),
-               disabled: true,
-               selModel: sm,
-               handler: function(b, e, rec) {
-                   var win = Ext.create('PVE.window.BackupConfig', {
-                       volume: rec.data.volid,
-                       pveSelNode: me.pveSelNode,
-                   });
-
-                   win.show();
+
+           me.store.load();
+           Proxmox.Utils.monStoreErrors(view, me.store);
+       },
+
+       getRecordGroups: function(records, expanded) {
+           let groups = {};
+           for (const item of records) {
+               const groupName = this.groupnameHelper(item.data);
+               groups[groupName] = {
+                   vmid: item.data.vmid,
+                   leaf: false,
+                   children: [],
+                   expanded: !!expanded[groupName],
+                   text: groupName,
+                   ctime: 0,
+                   format: item.data.format,
+                   volid: item.data.volid, // to preserve backup type 
information
+                   size: 0,
+                   iconCls: 
PVE.Utils.get_backup_type_icon_cls(item.data.volid, item.data.format),
+               };
+           }
+           return groups;
+       },
+
+       restoreHandler: function(button, event, rec) {
+           let me = this;
+           let vmtype = PVE.Utils.get_backup_type(rec.data.volid, 
rec.data.format);
+           let win = Ext.create('PVE.window.Restore', {
+               nodename: me.nodename,
+               volid: rec.data.volid,
+               volidText: PVE.Utils.render_storage_content(rec.data.volid, {}, 
rec),
+               vmtype: vmtype,
+               isPBS: me.view.isPBS,
+           });
+           win.on('destroy', () => me.reload());
+           win.show();
+       },
+
+       restoreFilesHandler: function(button, event, rec) {
+           let me = this;
+           let isVMArchive = PVE.Utils.volume_is_qemu_backup(rec.data.volid, 
rec.data.format);
+           Ext.create('Proxmox.window.FileBrowser', {
+               title: gettext('File Restore') + " - " + rec.data.text,
+               listURL: 
`/api2/json/nodes/localhost/storage/${me.storage}/file-restore/list`,
+               downloadURL: 
`/api2/json/nodes/localhost/storage/${me.storage}/file-restore/download`,
+               extraParams: {
+                   volume: rec.data.volid,
                },
-           },
-           {
-               xtype: 'proxmoxButton',
-               text: gettext('Edit Notes'),
-               disabled: true,
-               selModel: sm,
-               handler: function(b, e, rec) {
-                   let volid = rec.data.volid;
-                   Ext.create('Proxmox.window.Edit', {
-                       autoLoad: true,
-                       width: 600,
-                       height: 400,
-                       resizable: true,
-                       title: gettext('Notes'),
-                       url: 
`/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`,
+               archive: isVMArchive ? 'all' : undefined,
+               autoShow: true,
+           });
+       },
+
+       showConfigurationHandler: function(button, event, rec) {
+           let win = Ext.create('PVE.window.BackupConfig', {
+               volume: rec.data.volid,
+               pveSelNode: this.view.pveSelNode,
+           });
+           win.show();
+       },
+
+       editNotesHandler: function(button, event, rec) {
+           let me = this;
+           let volid = rec.data.volid;
+           Ext.create('Proxmox.window.Edit', {
+               autoLoad: true,
+               width: 600,
+               height: 400,
+               resizable: true,
+               title: gettext('Notes'),
+               url: 
`/api2/extjs/nodes/${me.nodename}/storage/${me.storage}/content/${volid}`,
+               layout: 'fit',
+               items: [
+                   {
+                       xtype: 'textarea',
                        layout: 'fit',
-                       items: [
-                           {
-                               xtype: 'textarea',
-                               layout: 'fit',
-                               name: 'notes',
-                               height: '100%',
-                           },
-                       ],
-                       listeners: {
-                           destroy: () => reload(),
-                       },
-                   }).show();
+                       name: 'notes',
+                       height: '100%',
+                   },
+               ],
+               listeners: {
+                   destroy: () => me.reload(),
                },
-           },
-           {
-               xtype: 'proxmoxButton',
-               text: gettext('Change Protection'),
-               disabled: true,
-               handler: function(button, event, record) {
-                   const volid = record.data.volid;
-                   Proxmox.Utils.API2Request({
-                       url: 
`/api2/extjs/nodes/${nodename}/storage/${me.storage}/content/${volid}`,
-                       method: 'PUT',
-                       waitMsgTarget: me,
-                       params: { 'protected': record.data.protected ? 0 : 1 },
-                       failure: (response) => Ext.Msg.alert('Error', 
response.htmlStatus),
-                       success: (response) => reload(),
-                   });
+           }).show();
+       },
+
+       changeProtectionHandler: function(button, event, rec) {
+           let me = this;
+           const volid = rec.data.volid;
+           Proxmox.Utils.API2Request({
+               url: 
`/api2/extjs/nodes/${me.nodename}/storage/${me.storage}/content/${volid}`,
+               method: 'PUT',
+               waitMsgTarget: button,
+               params: { 'protected': rec.data.protected ? 0 : 1 },
+               failure: (response) => Ext.Msg.alert('Error', 
response.htmlStatus),
+               success: (_) => me.reload(),
+           });
+       },
+
+       pruneGroupHandler: function(button, event, rec) {
+           let me = this;
+           let vmtype = PVE.Utils.get_backup_type(rec.data.volid, 
rec.data.format);
+           Ext.create('PVE.window.Prune', {
+               nodename: me.nodename,
+               storage: me.storage,
+               backup_id: rec.data.vmid,
+               backup_type: vmtype,
+               rec: rec,
+               listeners: {
+                   destroy: () => me.reload(),
                },
-           },
-           '-',
-           pruneButton,
-       );
-
-       if (isPBS) {
-           me.extraColumns = {
-               encrypted: {
-                   header: gettext('Encrypted'),
-                   dataIndex: 'encrypted',
-                   renderer: PVE.Utils.render_backup_encryption,
+           }).show();
+       },
+
+       removeHandler: function(button, event, rec) {
+           let me = this;
+           const volid = rec.data.volid;
+           Proxmox.Utils.API2Request({
+               url: 
`/nodes/${me.nodename}/storage/${me.storage}/content//${volid}`,
+               method: 'DELETE',
+               callback: () => me.reload(),
+               failure: response => Ext.Msg.alert(gettext('Error'), 
response.htmlStatus),
+           });
+       },
+
+       searchKeyupFn: function(field) {
+           let me = this;
+           me.getView().getStore().getFilters().removeByKey('volid');
+           me.getView().getStore().filter([
+               {
+                   property: 'volid',
+                   value: field.getValue(),
+                   anyMatch: true,
+                   caseSensitive: false,
+                   id: 'volid',
                },
-               verification: {
-                   header: gettext('Verify State'),
-                   dataIndex: 'verification',
-                   renderer: PVE.Utils.render_backup_verification,
+           ]);
+       },
+
+       searchClearHandler: function(field) {
+           field.triggers.clear.setVisible(false);
+           field.setValue(this.originalValue);
+           this.getView().getStore().clearFilter();
+       },
+
+       searchChangeFn: function(field, newValue, oldValue) {
+           if (newValue !== field.originalValue) {
+               field.triggers.clear.setVisible(true);
+           }
+       },
+
+       storageSelectorBoxReady: function(selector, width, height, eOpts) {
+           selector.setNodename(this.nodename);
+       },
+
+       storageSelectorChange: function(self, newValue, oldValue, eOpts) {
+           let me = this;
+           me.storage = newValue;
+           me.getView().getSelectionModel().deselectAll();
+           me.reload();
+       },
+
+       backupNowHandler: function(button, event) {
+           let me = this;
+           Ext.create('PVE.window.Backup', {
+               nodename: me.nodename,
+               vmid: me.vmid,
+               vmtype: me.vmtype,
+               storage: me.storage,
+               listeners: {
+                   close: () => me.reload(),
                },
-           };
-       }
+           }).show();
+       },
+    },
+
+    columns: [
+       {
+           xtype: 'treecolumn',
+           header: gettext("Backup Group"),
+           dataIndex: 'text',
+           renderer: function(value, _metadata, record) {
+               if (record.phantom) { return value; }
+               return PVE.Utils.render_storage_content(...arguments);
+           },
+           flex: 2,
+       },
+       {
+           header: gettext('Notes'),
+           flex: 1,
+           renderer: Ext.htmlEncode,
+           dataIndex: 'notes',
+       },
+       {
+           header: `<i class="fa fa-shield"></i>`,
+           tooltip: gettext('Protected'),
+           width: 30,
+           renderer: v => v ? `<i data-qtip="${gettext('Protected')}" 
class="fa fa-shield"></i>` : '',
+           sorter: (a, b) => (b.data.protected || 0) - (a.data.protected || 0),
+           dataIndex: 'protected',
+       },
+       {
+           header: gettext('Date'),
+           width: 150,
+           dataIndex: 'ctime',
+           xtype: 'datecolumn',
+           format: 'Y-m-d H:i:s',
+       },
+       {
+           header: gettext('Format'),
+           width: 100,
+           dataIndex: 'format',
+       },
+       {
+           header: gettext('Size'),
+           width: 100,
+           renderer: Proxmox.Utils.format_size,
+           dataIndex: 'size',
+       },
+       {
+           header: gettext('Encrypted'),
+           dataIndex: 'encrypted',
+           renderer: PVE.Utils.render_backup_encryption,
+           cbind: {
+               hidden: '{!isPBS}',
+           },
+       },
+       {
+           header: gettext('Verify State'),
+           dataIndex: 'verification',
+           renderer: PVE.Utils.render_backup_verification,
+           cbind: {
+               hidden: '{!isPBS}',
+           },
+       },
+    ],
+
+    tbar: [
+       {
+           xtype: 'button',
+           text: gettext('Backup now'),
+           handler: 'backupNowHandler',
+           reference: 'backupNowButton',
+       },
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Restore'),
+           handler: 'restoreHandler',
+           parentXType: "treepanel",
+           disabled: true,
+           enableFn: record => record.phantom === false,
+       },
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('File Restore'),
+           handler: 'restoreFilesHandler',
+           cbind: {
+               hidden: '{!isPBS}',
+           },
+           parentXType: "treepanel",
+           disabled: true,
+           enableFn: record => record.phantom === false,
+       },
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Show Configuration'),
+           handler: 'showConfigurationHandler',
+           parentXType: "treepanel",
+           disabled: true,
+           enableFn: record => record.phantom === false,
+       },
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Edit Notes'),
+           handler: 'editNotesHandler',
+           parentXType: "treepanel",
+           disabled: true,
+           enableFn: record => record.phantom === false,
+       },
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Change Protection'),
+           handler: 'changeProtectionHandler',
+           parentXType: "treepanel",
+           disabled: true,
+           enableFn: record => record.phantom === false,
+       },
+       '-',
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Prune group'),
+           setBackupGroup: function(backup) {
+               let me = this;
+               if (backup) {
+                   let volid = backup.volid;
+                   let vmid = backup.vmid;
+                   let format = backup.format;
 
-       me.callParent();
+                   let vmtype = PVE.Utils.get_backup_type(volid, format);
+                   if (vmid && vmtype) {
+                       me.setText(gettext('Prune group') + ` 
${vmtype}/${vmid}`);
+                       me.vmid = vmid;
+                       me.vmtype = vmtype;
+                       me.setDisabled(false);
+                       return;
+                   }
+               }
+               me.setText(gettext('Prune group'));
+               me.vmid = null;
+               me.vmtype = null;
+               me.setDisabled(true);
+           },
+           handler: 'pruneGroupHandler',
+           parentXType: "treepanel",
+           disabled: true,
+           reference: 'pruneButton',
+           enableFn: () => true,
+       },
+       {
+           xtype: 'proxmoxButton',
+           text: gettext('Remove'),
+           handler: 'removeHandler',
+           parentXType: 'treepanel',
+           disabled: true,
+           enableFn: record => record.phantom === false && 
!record?.data?.protected,
+           confirmMsg: function(rec) {
+               let name = rec.data.text;
+               return Ext.String.format(gettext('Are you sure you want to 
remove entry {0}'), `'${name}'`);
+           },
+       },
+       '->',
+       {
+           xtype: 'pveStorageSelector',
+           fieldLabel: gettext('Storage'),
+           storageContent: 'backup',
+           reference: 'storagesel',
+           listeners: {
+               change: 'storageSelectorChange',
+               boxready: 'storageSelectorBoxReady',
+           },
 
-       me.store.getSorters().clear();
-       me.store.setSorters([
-           {
-               property: 'vmid',
-               direction: 'ASC',
+       },
+       gettext('Search') + ':',
+       ' ',
+       {
+           xtype: 'textfield',
+           width: 200,
+           enableKeyEvents: true,
+           emptyText: gettext('Name, Format'),
+           listeners: {
+               keyup: {
+                   buffer: 500,
+                   fn: 'searchKeyupFn',
+               },
+               change: 'searchChangeFn',
            },
-           {
-               property: 'vdate',
-               direction: 'DESC',
+           triggers: {
+               clear: {
+                   cls: 'pmx-clear-trigger',
+                   weight: -1,
+                   hidden: true,
+                   handler: 'searchClearHandler',
+               },
            },
-       ]);
+       },
+       ],
+
+    listeners: {
+       activate: function() {
+           let me = this;
+           // only load on first activate to not load every tab switch
+           if (!me.firstLoad) {
+               me.getController().reload();
+               me.firstLoad = true;
+           }
+       },
+       selectionchange: function(model, srecords, eOpts) {
+           let pruneButton = this.getController().lookup('pruneButton');
+           if (srecords.length === 1) {
+               pruneButton.setBackupGroup(srecords[0].data);
+           } else {
+               pruneButton.setBackupGroup(null);
+           }
+       },
     },
 });
-- 
2.30.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to