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 72BC7B6EB for ; Thu, 7 Apr 2022 10:35:23 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 6481E285D for ; Thu, 7 Apr 2022 10:34:53 +0200 (CEST) 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 E62C42788 for ; Thu, 7 Apr 2022 10:34:47 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id B0F2A41A28 for ; Thu, 7 Apr 2022 10:34:47 +0200 (CEST) From: Matthias Heiserer To: pve-devel@lists.proxmox.com Date: Thu, 7 Apr 2022 10:34:37 +0200 Message-Id: <20220407083439.2413546-3-m.heiserer@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20220407083439.2413546-1-m.heiserer@proxmox.com> References: <20220407083439.2413546-1-m.heiserer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.005 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 - URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [me.store, me.storage, me.sm, view.store] Subject: [pve-devel] [PATCH v5 manager 2/4] ui: storage: 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: Thu, 07 Apr 2022 08:35:23 -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 --- changes from v4: always filter by type in guests for guests, never display backups of different type allow searching by displayed group name rename isStorage to fixedStorage code cleanup changes from v3: when guest backup is PBS, show encrypted and verify state columns and the "file restore" button BackupView: when searching, search in both format and volid changes from v2: Backup GUI: Use the new storage/BackupView instead of grid/BackupView. expand/collapse button allow restoring into current guest when clearing search field, only remove volid filter add "filter VMID" button, reorder tbar buttons filter vmid/type button storage less wide label sort first by backup group, then by vmid allow multisort rename groupnameHelper to getGroupName assume pveSelNode is set code cleanup: missing space, break long line remove duplicate / use AltTextButton use normal bind instead of cbind don't error on missing selection, e.g. after removal simplify `record-phantom === false` to `!record.phantom` disable pruning in 'Other' group use ternary instead of two ifs remove unnecessary code in activate listener use bind instead of boxready listener to set nodename of storageselector less horrific way of setting selection to falsy value; concat it using string template syntax Changes from v1: also replace grid/BackupView with storage/BackupView rename notPBS to PBS, make it available in the view Display combined size of backups for groups, except for PBS where deduplication happens remove timeout for API call liverestore iff PBS use get_backup_type helper + fix a bug where text was used instead of volid remove groupField, doesn't do anything for TreeStore only sort by text, ie. `(qemu|lxc)/` 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. For unknown/renamed backups, remove the exception in get_backup_type and group them in the folder "Other" cleanup: reorder statements, remove one-character variable, remove debug log, rename this to me add text field to model. fix date field in model remember expanded nodes and selection rename backup model use filterer only with correct store add BackupNow button to create backups beautify filtering www/manager6/storage/BackupView.js | 830 ++++++++++++++++++++++------- 1 file changed, 640 insertions(+), 190 deletions(-) diff --git a/www/manager6/storage/BackupView.js b/www/manager6/storage/BackupView.js index 2328c0fc..b50a23b0 100644 --- a/www/manager6/storage/BackupView.js +++ b/www/manager6/storage/BackupView.js @@ -1,223 +1,673 @@ -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', - showColumns: ['name', 'notes', 'protected', 'date', 'format', 'size'], + rootVisible: false, + multiColumnSort: true, + + title: gettext('Content'), + + viewModel: { + data: { + isPBS: false, + fixedStorage: true, + }, + }, + + controller: { + xclass: 'Ext.app.ViewController', + + collapseAllStatus: true, + + getGroupName: function(item) { + if (item.vmid) { + return PVE.Utils.get_backup_type(item.volid, item.format) + `/${item.vmid}`; + } else { + return 'Other'; + } + }, + + vmidFilter: function() { + let me = this; + return { + property: 'vmid', + id: 'vmid', + value: me.vmid, + exactMatch: true, + }; + }, + + init: function(view) { + let me = this; + me.storage = view.pveSelNode.data.storage; + me.nodename = view.pveSelNode.data.node; + me.vmid = view.pveSelNode.data.vmid; + me.backupType = view.pveSelNode.data.type; + me.vmtype = view.pveSelNode.data.type; + + me.store = Ext.create('Ext.data.Store', { + model: 'PVE.storage.BackupModel', + }); + me.store.on('load', me.onLoad, me); + // start with all groups expanded + me.store.on('load', () => me.lookup('collapseToggle').click(), me, { single: true }); + view.getStore().setConfig('filterer', 'bottomup'); + view.getStore().setSorters([ + { + property: 'groupName', + direction: 'ASC', + }, + { + property: 'ctime', + direction: 'DESC', + }, + ]); + + let viewModel = me.getViewModel(); + viewModel.set('nodename', me.nodename); + viewModel.set('isPBS', view.pluginType === 'pbs'); + + if (me.vmid) { + me.getView().getStore().filter([ + { + property: 'vmid', + id: 'vmid', + value: me.vmid, + exactMatch: true, + }, + { + property: 'backupType', + id: 'backupType', + value: me.backupType, + exactMatch: true, + }, + ]); + viewModel.set('fixedStorage', false); + } + Proxmox.Utils.monStoreErrors(view, me.store); + }, + + onLoad: function(store, records, success, operation) { + let me = this; + let view = me.getView(); + let selection = view.getSelection()?.[0]; + selection = selection + ? `${selection.parentNode.data.text}${selection.data.volid}` + : false; + + let storage = PVE.data.ResourceStore.getById( + `storage/${me.nodename}/${me.storage}`, + ); + let viewModel = me.getViewModel(); + viewModel.set('isPBS', storage.data.plugintype === 'pbs'); + + let expanded = {}; + view.getRootNode().cascadeBy({ + before: item => { + if (item.isExpanded() && !item.data.leaf) { + let id = item.data.text; + expanded[me.storage + '/' + id] = true; + return true; + } + return false; + }, + after: Ext.emptyFn, + }); + let groups = me.getRecordGroups(records, expanded); + + for (const item of records.map(i => i.data)) { + const groupName = me.getGroupName(item); + item.text = item.volid; + item.leaf = true; + item.iconCls = 'fa-file-o'; + item.backupType = PVE.Utils.get_backup_type(item.volid, item.format); + item.groupName = groupName; + groups[groupName].children.push(item); + groups[groupName].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 (viewModel.get('isPBS')) { + group.size = latest.size; + } + let num_verified = children.reduce((l, r) => l + r.verification === 'ok', 0); + group.verified = num_verified / children.length; + } - initComponent: function() { - var me = this; + let children = []; + Object.entries(groups).forEach(e => children.push(e[1])); + view.setRootNode({ + expanded: true, + children: children, + }); - var nodename = me.nodename = me.pveSelNode.data.node; - if (!nodename) { - throw "no node name specified"; - } + if (selection) { + let rootnode = view.getRootNode(); + let selected; + rootnode.cascade(node => { + if (selected) { // skip if already found + return false; + } + let id = node.parentNode?.data?.text + node.data?.volid; + if (id === selection) { + selected = node; + return false; + } + return true; + }); + if (selected) { + view.setSelection(selected); + view.getView().focusRow(selected); + } else { + me.getView().getSelectionModel().deselectAll(); + } + } + Proxmox.Utils.setErrorMask(view, false); + }, - var storage = me.storage = me.pveSelNode.data.storage; - if (!storage) { - throw "no storage ID specified"; - } + reload: function() { + let me = this; + let view = me.getView(); + if (!view.store || !me.store) { + console.warn('cannot reload, no store(s)'); + return; + } - me.content = 'backup'; + if (!me.storage) { + Proxmox.Utils.setErrorMask(view, true); + 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', + 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 = {}; + let me = this; + const storage = me.storage; + for (const item of records) { + const groupName = me.getGroupName(item.data); + groups[groupName] = { + vmid: item.data.vmid, + leaf: false, + children: [], + expanded: !!expanded[storage + '/' + groupName], + text: groupName, + ctime: 0, + groupName, + 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, + vmid: me.vmid, + isPBS: me.getViewModel().get('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, + }, + 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', + 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: me.getView(), + 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) { + let me = this; + me.getView().getStore().getFilters().removeByKey('searchFilter'); + let volidFilter = Ext.create('Ext.util.Filter', { + property: 'volid', + value: field.getValue(), + anyMatch: true, + caseSensitive: false, + }); + let formatFilter = Ext.create('Ext.util.Filter', { + property: 'format', + value: field.getValue(), + anyMatch: true, + caseSensitive: false, + }); + let groupFilter = Ext.create('Ext.util.Filter', { + property: 'groupName', + value: field.getValue(), + anyMatch: true, + caseSensitive: false, + }); + me.getView().getStore().filter({ + filterFn: item => volidFilter.filter(item.data) || + formatFilter.filter(item.data) || + groupFilter.filter(item.data), + id: 'searchFilter', + }); + }, + + searchClearHandler: function(field) { + field.triggers.clear.setVisible(false); + field.setValue(this.originalValue); + this.getView().getStore().getFilters().removeByKey('searchFilter'); + }, + + searchChangeFn: function(field, newValue, oldValue) { + if (newValue !== field.originalValue) { + field.triggers.clear.setVisible(true); + } + }, + + storageSelectorChange: function(self, newValue, oldValue, eOpts) { + let me = this; + if (!me.getViewModel().get('fixedStorage')) { + me.storage = newValue; + me.collapseAllStatus = true; + me.store.on('load', + () => me.lookup('collapseToggle').click(), + me, + { single: true }, + ); + 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(); + }, + + toggleCollapseHandler: function(button, event) { + let me = this; + let groups = me.getView().getRootNode().childNodes; + if (me.collapseAllStatus) { + groups.forEach(node => node.expand()); + button.setText(button.defaultText); + } else { + button.setText(button.altText); + groups.forEach(node => node.collapse()); + } + me.collapseAllStatus = !me.collapseAllStatus; + }, + + checkboxChangeHandler: function(checkbox, filterVMID) { + let me = this; + if (filterVMID) { + me.getView().getStore().filter(me.vmidFilter()); + } else { + let filters = me.getView().getStore().getFilters(); + filters.removeByKey('vmid'); + } + }, + }, + + columns: [ + { + xtype: 'treecolumn', + header: gettext("Backup Group"), + dataIndex: 'text', + renderer: function(value, _metadata, record) { + return record.phantom + ? value + : PVE.Utils.render_storage_content(...arguments); + }, + 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, + bind: { + hidden: '{!isPBS}', + }, + }, + { + header: gettext('Verify State'), + dataIndex: 'verification', + renderer: PVE.Utils.render_backup_verification, + bind: { + hidden: '{!isPBS}', + }, + }, + ], + + tbar: [ + { + xtype: 'button', + text: gettext('Backup now'), + handler: 'backupNowHandler', + hidden: true, + bind: { + hidden: '{fixedStorage}', + }, + }, + { + xtype: 'proxmoxButton', + text: gettext('Restore'), + handler: 'restoreHandler', + parentXType: "treepanel", disabled: true, - selModel: sm, + enableFn: record => !record.phantom, + }, + { + xtype: 'proxmoxButton', + text: gettext('File Restore'), + handler: 'restoreFilesHandler', + bind: { + hidden: '{!isPBS}', + }, + parentXType: "treepanel", + disabled: true, + enableFn: record => !record.phantom, + }, + { + xtype: 'proxmoxButton', + text: gettext('Show Configuration'), + handler: 'showConfigurationHandler', + parentXType: "treepanel", + disabled: true, + enableFn: record => !record.phantom, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit Notes'), + handler: 'editNotesHandler', + parentXType: "treepanel", + disabled: true, + enableFn: record => !record.phantom, + }, + { + xtype: 'proxmoxButton', + text: gettext('Change Protection'), + handler: 'changeProtectionHandler', + parentXType: "treepanel", + disabled: true, + enableFn: record => !record.phantom, + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Prune group'), setBackupGroup: function(backup) { + let me = this; if (backup) { - let name = backup.text; + let volid = backup.volid; 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'; - } - + let vmtype = PVE.Utils.get_backup_type(volid, format); 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: 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); + handler: 'pruneGroupHandler', + parentXType: "treepanel", + disabled: true, + reference: 'pruneButton', + enableFn: backup => backup.data.groupName !== 'Other', + }, + { + xtype: 'proxmoxButton', + text: gettext('Remove'), + handler: 'removeHandler', + parentXType: 'treepanel', + disabled: true, + enableFn: record => !record.phantom && !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: 'proxmoxAltTextButton', + text: gettext('Collapse all'), + defaultText: gettext('Collapse all'), + altText: gettext('Expand all'), + handler: 'toggleCollapseHandler', + reference: 'collapseToggle', + }, + { + xtype: 'pveStorageSelector', + fieldLabel: gettext('Storage') + ':', + labelAlign: 'right', + labelWidth: 65, + storageContent: 'backup', + listeners: { + change: 'storageSelectorChange', + }, + // hide by default so field doesn't flash when loading the page + hidden: true, + bind: { + hidden: '{fixedStorage}', + nodename: '{nodename}', }, - }); - - me.on('selectionchange', function(model, srecords, eOpts) { - 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); - }, + }, + '-', + { + xtype: 'checkbox', + boxLabel: gettext('Filter VMID'), + value: 'true', + listeners: { + change: 'checkboxChangeHandler', }, - ]; - 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(); - }, + // hide by default so field doesn't flash when loading the page + hidden: true, + bind: { + hidden: '{fixedStorage}', }, - { - 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: 'textfield', + label: gettext('Search'), + labelAlign: 'right', + width: 200, + enableKeyEvents: true, + emptyText: gettext('Name, Format'), + listeners: { + keyup: { + buffer: 500, + fn: 'searchKeyupFn', }, + change: 'searchChangeFn', }, - { - 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(), - }); + triggers: { + clear: { + cls: 'pmx-clear-trigger', + weight: -1, + hidden: true, + handler: 'searchClearHandler', }, }, - '-', - 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', - }, - ]); + listeners: { + activate: function() { + this.getController().reload(); + }, + 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