[pve-devel] [PATCH manager 3/3] Storage GUI: Rewrite backup content view as TreePanel.
Matthias Heiserer
m.heiserer at proxmox.com
Fri Mar 4 12:52:18 CET 2022
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.heiserer at proxmox.com>
---
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: `<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: '{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
More information about the pve-devel
mailing list