From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 4079464E24 for ; Fri, 4 Mar 2022 12:53:02 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 357B2439C for ; Fri, 4 Mar 2022 12:52:32 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id D1D654391 for ; Fri, 4 Mar 2022 12:52:29 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id A60C246ED5 for ; Fri, 4 Mar 2022 12:52:29 +0100 (CET) From: Matthias Heiserer To: pve-devel@lists.proxmox.com Date: Fri, 4 Mar 2022 12:52:18 +0100 Message-Id: <20220304115218.665615-3-m.heiserer@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20220304115218.665615-1-m.heiserer@proxmox.com> References: <20220304115218.665615-1-m.heiserer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.002 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH manager 3/3] Storage GUI: Rewrite backup content view as TreePanel. X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 04 Mar 2022 11:53:02 -0000 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 --- www/manager6/storage/BackupView.js | 620 ++++++++++++++++++++--------- 1 file changed, 433 insertions(+), 187 deletions(-) diff --git a/www/manager6/storage/BackupView.js b/www/manager6/storage/BackupView.js index 2328c0fc..124a57c9 100644 --- a/www/manager6/storage/BackupView.js +++ b/www/manager6/storage/BackupView.js @@ -1,36 +1,384 @@ -Ext.define('PVE.storage.BackupView', { - extend: 'PVE.storage.ContentView', +Ext.define('pve-data-store-backups', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'ctime', + date: 'date', + dateFormat: 'timestamp', + }, + 'format', + 'volid', + 'content', + 'vmid', + 'size', + 'protected', + 'notes', + ], +}); + +Ext.define('PVE.storage.BackupView', { + extend: 'Ext.tree.Panel', alias: 'widget.pveStorageBackupView', + mixins: ['Proxmox.Mixin.CBind'], + rootVisible: false, + + title: gettext('Content'), + + cbindData: function(initialCfg) { + return { + notPBS: initialCfg.pluginType !== 'pbs', + }; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + let me = this; + me.storage = view.pveSelNode.data.storage; + if (!me.storage) { + throw "No datastore specificed"; + } + me.nodename = view.pveSelNode.data.node; - showColumns: ['name', 'notes', 'protected', 'date', 'format', 'size'], + me.store = Ext.create('Ext.data.Store', { + model: 'pve-data-store-backups', + groupField: 'vmid', + filterer: 'bottomup', + }); + me.store.on('load', me.onLoad, me); + + view.getStore().setSorters([ + 'vmid', + 'text', + 'backup-time', + ]); + view.getStore().setConfig('filterer', 'bottomup'); + Proxmox.Utils.monStoreErrors(view, me.store); + }, + + onLoad: function(store, records, success, operation) { + let me = this; + let view = me.getView(); - initComponent: function() { - var me = this; + 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); - var nodename = me.nodename = me.pveSelNode.data.node; - if (!nodename) { - throw "no node name specified"; - } + for (const item of records.map(i => i.data)) { + item.text = item.volid; + item.leaf = true; + item.ctime = new Date(item.ctime * 1000); + item.iconCls = 'fa-file-o'; + groups[`${item.format}` + item.vmid].children.push(item); + } + + for (let [_name, group] of Object.entries(groups)) { + let c = group.children; + let latest = c.reduce((l, r) => l.ctime > r.ctime ? l : r); + let num_verified = c.reduce((l, r) => l + r.verification === 'ok', 0); + group.ctime = latest.ctime; + group.size = latest.size; + group.verified = num_verified / c.length; + } + + let children = []; + Object.entries(groups).forEach(e => children.push(e[1])); + view.setRootNode({ + expanded: true, + children: children, + }); - var storage = me.storage = me.pveSelNode.data.storage; - if (!storage) { - throw "no storage ID specified"; - } + Proxmox.Utils.setErrorMask(view, false); + }, - me.content = 'backup'; + reload: function() { + let me = this; + let view = me.getView(); + if (!view.store || !me.store) { + console.warn('cannot reload, no store(s)'); + return; + } - var sm = me.sm = Ext.create('Ext.selection.RowModel', {}); + let url = `/api2/json/nodes/${me.nodename}/storage/${me.storage}/content`; + me.store.setProxy({ + type: 'proxmox', + timeout: 60*1000, + url: url, + extraParams: { + content: 'backup', + }, + }); - var reload = function() { me.store.load(); - }; + Proxmox.Utils.monStoreErrors(view, me.store); + }, - let pruneButton = Ext.create('Proxmox.button.Button', { - text: gettext('Prune group'), + getRecordGroups: function(records, expanded) { + let groups = {}; + for (const item of records) { + groups[`${item.data.format}` + item.data.vmid] = { + vmid: item.data.vmid, + leaf: false, + children: [], + expanded: !!expanded[item.data.vmid], + text: item.data.vmid, + ctime: 0, + format: item.data.format, + volid: "volid", + content: "content", + size: 0, + iconCls: PVE.Utils.get_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.isPBS, + view: me.view, + }); + 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, + }, + archive: isVMArchive ? 'all' : undefined, + autoShow: true, + }); + }, + + showConfigurationHandler: function(button, event, rec) { + let win = Ext.create('PVE.window.BackupConfig', { + volume: rec.data.volid, + node: this.nodename, + }); + 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', + name: 'notes', + height: '100%', + }, + ], + listeners: { + destroy: () => me.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(), + }, + }).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) { + this.getView().getStore().clearFilter(true); + this.getView().getStore().filter([ + { + property: 'volid', + value: field.getValue(), + anyMatch: true, + caseSensitive: false, + }, + ]); + }, + + 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); + } + }, + }, + + columns: [ + { + xtype: 'treecolumn', + header: gettext("Backup Group"), + dataIndex: 'text', + flex: 2, + }, + { + header: gettext('Notes'), + flex: 1, + renderer: Ext.htmlEncode, + dataIndex: 'notes', + }, + { + header: ``, + tooltip: gettext('Protected'), + width: 30, + renderer: v => v ? `` : '', + 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: '{notPBS}', + }, + }, + { + header: gettext('Verify State'), + dataIndex: 'verification', + renderer: PVE.Utils.render_backup_verification, + cbind: { + hidden: '{notPBS}', + }, + }, + ], + + tbar: [ + { + 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: '{notPBS}', + }, + 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, - selModel: sm, + 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 name = backup.text; let vmid = backup.vmid; @@ -38,186 +386,84 @@ Ext.define('PVE.storage.BackupView', { let vmtype; if (name.startsWith('vzdump-lxc-') || format === "pbs-ct") { - vmtype = 'lxc'; + vmtype = 'lxc'; } else if (name.startsWith('vzdump-qemu-') || format === "pbs-vm") { - vmtype = 'qemu'; + vmtype = 'qemu'; } - if (vmid && vmtype) { - this.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`); - this.vmid = vmid; - this.vmtype = vmtype; - this.setDisabled(false); + me.setText(gettext('Prune group') + ` ${vmtype}/${vmid}`); + me.vmid = vmid; + me.vmtype = vmtype; + me.setDisabled(false); return; } } - this.setText(gettext('Prune group')); - this.vmid = null; - this.vmtype = null; - this.setDisabled(true); + 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) { + console.log("controller:", this.getController()); + let name = rec.data.text; + return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${name}'`); + }, + }, + '->', + gettext('Search') + ':', + ' ', + { + xtype: 'textfield', + width: 200, + enableKeyEvents: true, + emptyText: gettext('Name, Format'), + listeners: { + keyup: { + buffer: 500, + fn: 'searchKeyupFn', + }, + change: 'searchChangeFn', }, - 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); + triggers: { + clear: { + cls: 'pmx-clear-trigger', + weight: -1, + hidden: true, + handler: 'searchClearHandler', + }, }, - }); + }, + ], - me.on('selectionchange', function(model, srecords, eOpts) { + 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); } - }); - - 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, - }); - }, - }); - } - 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(); - }, - }, - { - 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}`, - layout: 'fit', - items: [ - { - xtype: 'textarea', - layout: 'fit', - name: 'notes', - height: '100%', - }, - ], - listeners: { - destroy: () => reload(), - }, - }).show(); - }, - }, - { - 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(), - }); - }, - }, - '-', - pruneButton, - ); - - if (isPBS) { - me.extraColumns = { - encrypted: { - header: gettext('Encrypted'), - dataIndex: 'encrypted', - renderer: PVE.Utils.render_backup_encryption, - }, - verification: { - header: gettext('Verify State'), - dataIndex: 'verification', - renderer: PVE.Utils.render_backup_verification, - }, - }; - } - - me.callParent(); - - me.store.getSorters().clear(); - me.store.setSorters([ - { - property: 'vmid', - direction: 'ASC', - }, - { - property: 'vdate', - direction: 'DESC', - }, - ]); + }, }, }); -- 2.30.2