public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Matthias Heiserer <m.heiserer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v5 manager 2/4] ui: storage: Rewrite backup content view as TreePanel.
Date: Thu,  7 Apr 2022 10:34:37 +0200	[thread overview]
Message-ID: <20220407083439.2413546-3-m.heiserer@proxmox.com> (raw)
In-Reply-To: <20220407083439.2413546-1-m.heiserer@proxmox.com>

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@proxmox.com>
---
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)/<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.
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: `<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,
+	    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





  parent reply	other threads:[~2022-04-07  8:35 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-04-07  8:34 [pve-devel] [PATCH v5 manager 0/4] BackupView " Matthias Heiserer
2022-04-07  8:34 ` [pve-devel] [PATCH v5 manager 1/4] ui: Utils: Helpers for backup type and icon Matthias Heiserer
2022-04-07  8:34 ` Matthias Heiserer [this message]
2022-04-07  8:34 ` [pve-devel] [PATCH v5 manager 3/4] ui: delete BackupView and replace it with the new Tree BackupView Matthias Heiserer
2022-04-07  8:34 ` [pve-devel] [PATCH v5 manager 4/4] ui: content view: remove dead code Matthias Heiserer
2022-04-07 10:37 ` [pve-devel] [PATCH v5 manager 0/4] BackupView as TreePanel Fabian Ebner

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20220407083439.2413546-3-m.heiserer@proxmox.com \
    --to=m.heiserer@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal