[pbs-devel] [RFC v2 proxmox-backup 19/21] ui: add recover for trashed items tab to datastore panel
Christian Ebner
c.ebner at proxmox.com
Thu May 8 15:05:53 CEST 2025
Display a dedicated recover trashed tab which allows to inspect and
recover trashed items.
This is based on the pre-existing contents tab but drops any actions
which make no sense for the given context, such as editing of group
ownership, notes, verification, ecc.
Signed-off-by: Christian Ebner <c.ebner at proxmox.com>
---
www/Makefile | 1 +
www/datastore/Panel.js | 8 +
www/datastore/RecoverTrashed.js | 876 ++++++++++++++++++++++++++++++++
3 files changed, 885 insertions(+)
create mode 100644 www/datastore/RecoverTrashed.js
diff --git a/www/Makefile b/www/Makefile
index 44c5fa133..aa8955460 100644
--- a/www/Makefile
+++ b/www/Makefile
@@ -115,6 +115,7 @@ JSSRC= \
datastore/Panel.js \
datastore/DataStoreListSummary.js \
datastore/DataStoreList.js \
+ datastore/RecoverTrashed.js \
ServerStatus.js \
ServerAdministration.js \
NodeNotes.js \
diff --git a/www/datastore/Panel.js b/www/datastore/Panel.js
index ad9fc10fe..386b62284 100644
--- a/www/datastore/Panel.js
+++ b/www/datastore/Panel.js
@@ -99,6 +99,14 @@ Ext.define('PBS.DataStorePanel', {
datastore: '{datastore}',
},
},
+ {
+ xtype: 'pbsDataStoreRecoverTrashed',
+ itemId: 'trashed',
+ iconCls: 'fa fa-rotate-left',
+ cbind: {
+ datastore: '{datastore}',
+ },
+ },
],
initComponent: function() {
diff --git a/www/datastore/RecoverTrashed.js b/www/datastore/RecoverTrashed.js
new file mode 100644
index 000000000..2257a8cd3
--- /dev/null
+++ b/www/datastore/RecoverTrashed.js
@@ -0,0 +1,876 @@
+Ext.define('PBS.DataStoreRecoverTrashed', {
+ extend: 'Ext.tree.Panel',
+ alias: 'widget.pbsDataStoreRecoverTrashed',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ rootVisible: false,
+
+ title: gettext('Recover Trashed'),
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ init: function(view) {
+ if (!view.datastore) {
+ throw "no datastore specified";
+ }
+
+ this.store = Ext.create('Ext.data.Store', {
+ model: 'pbs-data-store-snapshots',
+ groupField: 'backup-group',
+ });
+ this.store.on('load', this.onLoad, this);
+
+ view.getStore().setSorters([
+ 'sortWeight',
+ 'text',
+ 'backup-time',
+ ]);
+ },
+
+ control: {
+ '#': { // view
+ rowdblclick: 'rowDoubleClicked',
+ },
+ 'pbsNamespaceSelector': {
+ change: 'nsChange',
+ },
+ },
+
+ rowDoubleClicked: function(table, rec, el, rowId, ev) {
+ if (rec?.data?.ty === 'ns' && !rec.data.root) {
+ this.nsChange(null, rec.data.ns);
+ }
+ },
+
+ nsChange: function(field, value) {
+ let view = this.getView();
+ if (field === null) {
+ field = view.down('pbsNamespaceSelector');
+ field.setValue(value);
+ return;
+ }
+ view.namespace = value;
+ this.reload();
+ },
+
+ reload: function() {
+ let view = this.getView();
+
+ if (!view.store || !this.store) {
+ console.warn('cannot reload, no store(s)');
+ return;
+ }
+
+ let url = `/api2/json/admin/datastore/${view.datastore}/snapshots?trashed=1`;
+ if (view.namespace && view.namespace !== '') {
+ url += `&ns=${encodeURIComponent(view.namespace)}`;
+ }
+ this.store.setProxy({
+ type: 'proxmox',
+ timeout: 300*1000, // 5 minutes, we should make that api call faster
+ url: url,
+ });
+
+ this.store.load();
+ },
+
+ getRecordGroups: function(records) {
+ let groups = {};
+
+ for (const item of records) {
+ var btype = item.data["backup-type"];
+ let group = btype + "/" + item.data["backup-id"];
+
+ if (groups[group] !== undefined) {
+ continue;
+ }
+
+ var cls = PBS.Utils.get_type_icon_cls(btype);
+ if (cls === "") {
+ console.warn(`got unknown backup-type '${btype}'`);
+ continue; // FIXME: auto render? what do?
+ }
+
+ groups[group] = {
+ text: group,
+ leaf: false,
+ iconCls: "fa " + cls,
+ expanded: false,
+ backup_type: item.data["backup-type"],
+ backup_id: item.data["backup-id"],
+ children: [],
+ };
+ }
+
+ return groups;
+ },
+
+ loadNamespaceFromSameLevel: async function() {
+ let view = this.getView();
+ try {
+ let url = `/api2/extjs/admin/datastore/${view.datastore}/namespace?max-depth=1`;
+ if (view.namespace && view.namespace !== '') {
+ url += `&parent=${encodeURIComponent(view.namespace)}`;
+ }
+ url += '&include-trashed=1';
+ let { result: { data: ns } } = await Proxmox.Async.api2({ url });
+ return ns;
+ } catch (err) {
+ console.debug(err);
+ }
+ return [];
+ },
+
+ onLoad: async function(store, records, success, operation) {
+ let me = this;
+ let view = this.getView();
+
+ let namespaces = await me.loadNamespaceFromSameLevel();
+
+ if (!success) {
+ // TODO also check error code for != 403 ?
+ if (namespaces.length === 0) {
+ let error = Proxmox.Utils.getResponseErrorMessage(operation.getError());
+ Proxmox.Utils.setErrorMask(view.down('treeview'), error);
+ return;
+ } else {
+ records = [];
+ }
+ } else {
+ Proxmox.Utils.setErrorMask(view.down('treeview'));
+ }
+
+ let groups = this.getRecordGroups(records);
+
+ let selected;
+ let expanded = {};
+
+ view.getSelection().some(function(item) {
+ let id = item.data.text;
+ if (item.data.leaf) {
+ id = item.parentNode.data.text + id;
+ }
+ selected = id;
+ return true;
+ });
+
+ 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,
+ });
+
+ for (const item of records) {
+ let group = item.data["backup-type"] + "/" + item.data["backup-id"];
+ let children = groups[group].children;
+
+ let data = item.data;
+
+ data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
+ data.leaf = false;
+ data.cls = 'no-leaf-icons';
+ data.matchesFilter = true;
+ data.ty = 'dir';
+
+ data.expanded = !!expanded[data.text];
+
+ data.children = [];
+ for (const file of data.files) {
+ file.text = file.filename;
+ file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
+ file.fingerprint = data.fingerprint;
+ file.leaf = true;
+ file.matchesFilter = true;
+ file.ty = 'file';
+
+ data.children.push(file);
+ }
+
+ children.push(data);
+ }
+
+ let children = [];
+ for (const [name, group] of Object.entries(groups)) {
+ let last_backup = 0;
+ let crypt = {
+ none: 0,
+ mixed: 0,
+ 'sign-only': 0,
+ encrypt: 0,
+ };
+ for (let item of group.children) {
+ crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
+ if (item["backup-time"] > last_backup && item.size !== null) {
+ last_backup = item["backup-time"];
+ group["backup-time"] = last_backup;
+ group["last-comment"] = item.comment;
+ group.files = item.files;
+ group.size = item.size;
+ group.owner = item.owner;
+ }
+ }
+ group.count = group.children.length;
+ group.matchesFilter = true;
+ crypt.count = group.count;
+ group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
+ group.expanded = !!expanded[name];
+ group.sortWeight = 0;
+ group.ty = 'group';
+ children.push(group);
+ }
+
+ for (const item of namespaces) {
+ if (item.ns === view.namespace || (!view.namespace && item.ns === '')) {
+ continue;
+ }
+ children.push({
+ text: item.ns,
+ iconCls: 'fa fa-object-group',
+ expanded: true,
+ expandable: false,
+ ns: (view.namespaces ?? '') !== '' ? `/${item.ns}` : item.ns,
+ ty: 'ns',
+ sortWeight: 10,
+ leaf: true,
+ });
+ }
+
+ let isRootNS = !view.namespace || view.namespace === '';
+ let rootText = isRootNS
+ ? gettext('Root Namespace')
+ : Ext.String.format(gettext("Namespace '{0}'"), view.namespace);
+
+ let topNodes = [];
+ if (!isRootNS) {
+ let parentNS = view.namespace.split('/').slice(0, -1).join('/');
+ topNodes.push({
+ text: `.. (${parentNS === '' ? gettext('Root') : parentNS})`,
+ iconCls: 'fa fa-level-up',
+ ty: 'ns',
+ ns: parentNS,
+ sortWeight: -10,
+ leaf: true,
+ });
+ }
+ topNodes.push({
+ text: rootText,
+ iconCls: "fa fa-" + (isRootNS ? 'database' : 'object-group'),
+ expanded: true,
+ expandable: false,
+ sortWeight: -5,
+ root: true, // fake root
+ isRootNS,
+ ty: 'ns',
+ children: children,
+ });
+
+ view.setRootNode({
+ expanded: true,
+ children: topNodes,
+ });
+
+ if (!children.length) {
+ view.setEmptyText(Ext.String.format(
+ gettext('No accessible snapshots found in namespace {0}'),
+ view.namespace && view.namespace !== '' ? `'${view.namespace}'`: gettext('Root'),
+ ));
+ }
+
+ if (selected !== undefined) {
+ let selection = view.getRootNode().findChildBy(function(item) {
+ let id = item.data.text;
+ if (item.data.leaf) {
+ id = item.parentNode.data.text + id;
+ }
+ return selected === id;
+ }, undefined, true);
+ if (selection) {
+ view.setSelection(selection);
+ view.getView().focusRow(selection);
+ }
+ }
+
+ Proxmox.Utils.setErrorMask(view, false);
+ if (view.getStore().getFilters().length > 0) {
+ let searchBox = me.lookup("searchbox");
+ let searchvalue = searchBox.getValue();
+ me.search(searchBox, searchvalue);
+ }
+ },
+
+ recoverNamespace: function(data) {
+ let me = this;
+ let view = me.getView();
+ if (!view.namespace || view.namespace === '') {
+ console.warn('recoverNamespace called with root NS!');
+ return;
+ }
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: Ext.Msg.WARNING,
+ message: gettext('Are you sure you want to recover all namespace contents?'),
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: 'yes',
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ let params = { "ns": view.namespace };
+
+ Proxmox.Utils.API2Request({
+ url: `/admin/datastore/${view.datastore}/recover-namespace`,
+ params,
+ method: 'POST',
+ waitMsgTarget: view,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: me.reload.bind(me),
+ });
+ },
+ });
+ },
+
+ forgetNamespace: function(data) {
+ let me = this;
+ let view = me.getView();
+ if (!view.namespace || view.namespace === '') {
+ console.warn('forgetNamespace called with root NS!');
+ return;
+ }
+ let nsParts = view.namespace.split('/');
+ let nsName = nsParts.pop();
+ let parentNS = nsParts.join('/');
+
+ Ext.create('PBS.window.NamespaceDelete', {
+ datastore: view.datastore,
+ namespace: view.namespace,
+ item: { id: nsName },
+ apiCallDone: success => {
+ if (success) {
+ view.namespace = parentNS; // move up before reload to avoid "ENOENT" error
+ me.reload();
+ }
+ },
+ });
+ },
+
+ recoverGroup: function(data) {
+ let me = this;
+ let view = me.getView();
+
+ let params = {
+ "backup-type": data.backup_type,
+ "backup-id": data.backup_id,
+ };
+ if (view.namespace && view.namespace !== '') {
+ params.ns = view.namespace;
+ }
+
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: Ext.Msg.WARNING,
+ message: Ext.String.format(gettext('Are you sure you want to recover group {0}'), `'${data.text}'`),
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: 'yes',
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: `/admin/datastore/${view.datastore}/recover-group`,
+ params,
+ method: 'POST',
+ waitMsgTarget: view,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: me.reload.bind(me),
+ });
+ },
+ });
+ },
+
+ forgetGroup: function(data) {
+ let me = this;
+ let view = me.getView();
+
+ let params = {
+ "backup-type": data.backup_type,
+ "backup-id": data.backup_id,
+ "skip-trash": true,
+ };
+ if (view.namespace && view.namespace !== '') {
+ params.ns = view.namespace;
+ }
+
+ Ext.create('Proxmox.window.SafeDestroy', {
+ url: `/admin/datastore/${view.datastore}/groups`,
+ params,
+ item: {
+ id: data.text,
+ },
+ autoShow: true,
+ taskName: 'forget-group',
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+
+ recoverSnapshot: function(data) {
+ let me = this;
+ let view = me.getView();
+
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: Ext.Msg.WARNING,
+ message: Ext.String.format(gettext('Are you sure you want to recover snapshot {0}'), `'${data.text}'`),
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: 'yes',
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ let params = {
+ "backup-type": data["backup-type"],
+ "backup-id": data["backup-id"],
+ "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
+ };
+ if (view.namespace && view.namespace !== '') {
+ params.ns = view.namespace;
+ }
+
+ //TODO adapt to recover api endpoint
+ Proxmox.Utils.API2Request({
+ url: `/admin/datastore/${view.datastore}/recover-snapshot`,
+ params,
+ method: 'POST',
+ waitMsgTarget: view,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: me.reload.bind(me),
+ });
+ },
+ });
+ },
+
+ forgetSnapshot: function(data) {
+ let me = this;
+ let view = me.getView();
+
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: Ext.Msg.WARNING,
+ message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: 'no',
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+ let params = {
+ "backup-type": data["backup-type"],
+ "backup-id": data["backup-id"],
+ "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
+ "skip-trash": true,
+ };
+ if (view.namespace && view.namespace !== '') {
+ params.ns = view.namespace;
+ }
+
+ Proxmox.Utils.API2Request({
+ url: `/admin/datastore/${view.datastore}/snapshots`,
+ params,
+ method: 'DELETE',
+ waitMsgTarget: view,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: me.reload.bind(me),
+ });
+ },
+ });
+ },
+
+ onRecover: function(table, rI, cI, item, e, { data }) {
+ let me = this;
+ let view = this.getView();
+ if ((data.ty !== 'group' && data.ty !== 'dir' && data.ty !== 'ns') || !view.datastore) {
+ return;
+ }
+
+ if (data.ty === 'ns') {
+ me.recoverNamespace(data);
+ } else if (data.ty === 'dir') {
+ me.recoverSnapshot(data);
+ } else {
+ me.recoverGroup(data);
+ }
+ },
+
+ onForget: function(table, rI, cI, item, e, { data }) {
+ let me = this;
+ let view = this.getView();
+ if ((data.ty !== 'group' && data.ty !== 'dir' && data.ty !== 'ns') || !view.datastore) {
+ return;
+ }
+
+ if (data.ty === 'ns') {
+ me.forgetNamespace(data);
+ } else if (data.ty === 'dir') {
+ me.forgetSnapshot(data);
+ } else {
+ me.forgetGroup(data);
+ }
+ },
+
+ // opens a namespace browser
+ openBrowser: function(tv, rI, Ci, item, e, rec) {
+ let me = this;
+ if (rec.data.ty === 'ns') {
+ me.nsChange(null, rec.data.ns);
+ }
+ },
+
+ filter: function(item, value) {
+ if (item.data.text.indexOf(value) !== -1) {
+ return true;
+ }
+
+ if (item.data.owner && item.data.owner.indexOf(value) !== -1) {
+ return true;
+ }
+
+ return false;
+ },
+
+ search: function(tf, value) {
+ let me = this;
+ let view = me.getView();
+ let store = view.getStore();
+ if (!value && value !== 0) {
+ store.clearFilter();
+ // only collapse the children below our toplevel namespace "root"
+ store.getRoot().lastChild.collapseChildren(true);
+ tf.triggers.clear.setVisible(false);
+ return;
+ }
+ tf.triggers.clear.setVisible(true);
+ if (value.length < 2) return;
+ Proxmox.Utils.setErrorMask(view, true);
+ // we do it a little bit later for the error mask to work
+ setTimeout(function() {
+ store.clearFilter();
+ store.getRoot().collapseChildren(true);
+
+ store.beginUpdate();
+ store.getRoot().cascadeBy({
+ before: function(item) {
+ if (me.filter(item, value)) {
+ item.set('matchesFilter', true);
+ if (item.parentNode && item.parentNode.id !== 'root') {
+ item.parentNode.childmatches = true;
+ }
+ return false;
+ }
+ return true;
+ },
+ after: function(item) {
+ if (me.filter(item, value) || item.id === 'root' || item.childmatches) {
+ item.set('matchesFilter', true);
+ if (item.parentNode && item.parentNode.id !== 'root') {
+ item.parentNode.childmatches = true;
+ }
+ if (item.childmatches) {
+ item.expand();
+ }
+ } else {
+ item.set('matchesFilter', false);
+ }
+ delete item.childmatches;
+ },
+ });
+ store.endUpdate();
+
+ store.filter((item) => !!item.get('matchesFilter'));
+ Proxmox.Utils.setErrorMask(view, false);
+ }, 10);
+ },
+ },
+
+ listeners: {
+ activate: function() {
+ let me = this;
+ me.getController().reload();
+ },
+ itemcontextmenu: function(panel, record, item, index, event) {
+ event.stopEvent();
+ let title;
+ let view = panel.up('pbsDataStoreRecoverTrashed');
+ let controller = view.getController();
+ let createControllerCallback = function(name) {
+ return function() {
+ controller[name](view, undefined, undefined, undefined, undefined, record);
+ };
+ };
+ if (record.data.ty === 'group') {
+ title = gettext('Group');
+ } else if (record.data.ty === 'dir') {
+ title = gettext('Snapshot');
+ } else if (record.data.ty === 'ns') {
+ title = gettext('Namespace');
+ }
+ if (title) {
+ let menu = Ext.create('PBS.datastore.RecoverTrashedContextMenu', {
+ title: title,
+ onRecover: createControllerCallback('onRecover'),
+ onForget: createControllerCallback('onForget'),
+ });
+ menu.showAt(event.getXY());
+ }
+ },
+ },
+
+ columns: [
+ {
+ xtype: 'treecolumn',
+ header: gettext("Backup Group"),
+ dataIndex: 'text',
+ renderer: (value, meta, record) => {
+ if (record.data.protected) {
+ return `${value} (${gettext('protected')})`;
+ }
+ return value;
+ },
+ flex: 1,
+ },
+ {
+ text: gettext('Comment'),
+ dataIndex: 'comment',
+ flex: 1,
+ renderer: (v, meta, record) => {
+ let data = record.data;
+ if (!data || data.leaf || data.root) {
+ return '';
+ }
+
+ let additionalClasses = "";
+ if (!v) {
+ if (!data.expanded) {
+ v = data['last-comment'] ?? '';
+ additionalClasses = 'pmx-opacity-75';
+ } else {
+ v = '';
+ }
+ }
+ v = Ext.String.htmlEncode(v);
+ return `<span class="snapshot-comment-column ${additionalClasses}">${v}</span>`;
+ },
+ },
+ {
+ header: gettext('Actions'),
+ xtype: 'actioncolumn',
+ dataIndex: 'text',
+ width: 80,
+ items: [
+ {
+ handler: 'onRecover',
+ getTip: (v, m, { data }) => {
+ let tip = '{0}';
+ if (data.ty === 'ns') {
+ tip = gettext("Recover all namespace contents");
+ } else if (data.ty === 'dir') {
+ tip = gettext("Recover snapshot '{0}'");
+ } else if (data.ty === 'group') {
+ tip = gettext("Recover group '{0}'");
+ }
+ return Ext.String.format(tip, v);
+ },
+ getClass: (v, m, { data }) =>
+ (data.ty === 'ns' && !data.isRootNS && data.ns === undefined) ||
+ data.ty === 'group' || data.ty === 'dir'
+ ? 'fa fa-rotate-left'
+ : 'pmx-hidden',
+ isActionDisabled: (v, r, c, i, { data }) => false,
+ },
+ '->',
+ {
+ handler: 'onForget',
+ getTip: (v, m, { data }) => {
+ let tip = '{0}';
+ if (data.ty === 'ns') {
+ tip = gettext("Permanently forget namespace contents '{0}'");
+ } else if (data.ty === 'dir') {
+ tip = gettext("Permanently forget snapshot '{0}'");
+ } else if (data.ty === 'group') {
+ tip = gettext("Permanently forget group '{0}'");
+ }
+ return Ext.String.format(tip, v);
+ },
+ getClass: (v, m, { data }) =>
+ (data.ty === 'ns' && !data.isRootNS && data.ns === undefined) ||
+ data.ty === 'group' || data.ty === 'dir'
+ ? 'fa critical fa-trash-o'
+ : 'pmx-hidden',
+ isActionDisabled: (v, r, c, i, { data }) => false,
+ },
+ {
+ handler: 'openBrowser',
+ tooltip: gettext('Browse'),
+ getClass: (v, m, { data }) => data.ty === 'ns' && !data.root
+ ? 'fa fa-folder-open-o'
+ : 'pmx-hidden',
+ isActionDisabled: (v, r, c, i, { data }) => data.ty !== 'ns',
+ },
+ ],
+ },
+ {
+ xtype: 'datecolumn',
+ header: gettext('Backup Time'),
+ sortable: true,
+ dataIndex: 'backup-time',
+ format: 'Y-m-d H:i:s',
+ width: 150,
+ },
+ {
+ header: gettext("Size"),
+ sortable: true,
+ dataIndex: 'size',
+ renderer: (v, meta, { data }) => {
+ if ((data.text === 'client.log.blob' && v === undefined) || (data.ty !== 'dir' && data.ty !== 'file')) {
+ return '';
+ }
+ if (v === undefined || v === null) {
+ meta.tdCls = "x-grid-row-loading";
+ return '';
+ }
+ return Proxmox.Utils.format_size(v);
+ },
+ },
+ {
+ xtype: 'numbercolumn',
+ format: '0',
+ header: gettext("Count"),
+ sortable: true,
+ width: 75,
+ align: 'right',
+ dataIndex: 'count',
+ },
+ {
+ header: gettext("Owner"),
+ sortable: true,
+ dataIndex: 'owner',
+ },
+ {
+ header: gettext('Encrypted'),
+ dataIndex: 'crypt-mode',
+ renderer: (v, meta, record) => {
+ if (record.data.size === undefined || record.data.size === null) {
+ return '';
+ }
+ if (v === -1) {
+ return '';
+ }
+ let iconCls = PBS.Utils.cryptIconCls[v] || '';
+ let iconTxt = "";
+ if (iconCls) {
+ iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `;
+ }
+ let tip;
+ if (v !== PBS.Utils.cryptmap.indexOf('none') && record.data.fingerprint !== undefined) {
+ tip = "Key: " + PBS.Utils.renderKeyID(record.data.fingerprint);
+ }
+ let txt = (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText;
+ if (record.data.ty === 'group' || tip === undefined) {
+ return txt;
+ } else {
+ return `<span data-qtip="${tip}">${txt}</span>`;
+ }
+ },
+ },
+ ],
+
+ tbar: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: 'reload',
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ html: gettext('Namespace') + ':',
+ },
+ {
+ xtype: 'pbsNamespaceSelector',
+ width: 200,
+ cbind: {
+ datastore: '{datastore}',
+ },
+ },
+ '-',
+ {
+ xtype: 'tbtext',
+ html: gettext('Search'),
+ },
+ {
+ xtype: 'textfield',
+ reference: 'searchbox',
+ emptyText: gettext('group, date or owner'),
+ triggers: {
+ clear: {
+ cls: 'pmx-clear-trigger',
+ weight: -1,
+ hidden: true,
+ handler: function() {
+ this.triggers.clear.setVisible(false);
+ this.setValue('');
+ },
+ },
+ },
+ listeners: {
+ change: {
+ fn: 'search',
+ buffer: 500,
+ },
+ },
+ },
+ ],
+});
+
+Ext.define('PBS.datastore.RecoverTrashedContextMenu', {
+ extend: 'Ext.menu.Menu',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ onRecover: undefined,
+ onForget: undefined,
+
+ items: [
+ {
+ text: gettext('Recover'),
+ iconCls: 'fa critical fa-rotate-left',
+ handler: function() { this.up('menu').onRecover(); },
+ cbind: {
+ hidden: '{!onRecover}',
+ },
+ },
+ {
+ text: gettext('Remove'),
+ iconCls: 'fa critical fa-trash-o',
+ handler: function() { this.up('menu').onForget(); },
+ cbind: {
+ hidden: '{!onForget}',
+ },
+ },
+ ],
+});
--
2.39.5
More information about the pbs-devel
mailing list