From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 41F631FF0AF for ; Fri, 06 Nov 2026 18:24:53 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id E7E4E2175F; Fri, 06 Nov 2026 18:23:47 +0100 (CET) From: Christian Ebner To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup v2 35/43] ui: show snapshot retention in datastore contents and allow extension Date: Fri, 6 Nov 2026 18:22:27 +0100 Message-ID: <20261106172235.890542-36-c.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261106172235.890542-1-c.ebner@proxmox.com> References: <20261106172235.890542-1-c.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793985780186 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.442 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: FTKHGEQCDL4UVWKFLTRFU6VRMVYITZPR X-Message-ID-Hash: FTKHGEQCDL4UVWKFLTRFU6VRMVYITZPR X-MailFrom: c.ebner@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Follows along the line of how protection toggle and attributes are being handled. Extend the shown attribute next to the snapshot name to also include the retention timestamp if set. Further, display an hourglass icon next to the protected shield icon and show retention timestamp when hovering. If the user is logged in as root@pam, also allow to act on the retention by either setting or extending it. For regular users this is not possible, the icon shown faded instead. Signed-off-by: Christian Ebner --- www/Makefile | 1 + www/datastore/Content.js | 103 +++++++++++++++++++++++++++++++++++- www/window/RetentionEdit.js | 56 ++++++++++++++++++++ 3 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 www/window/RetentionEdit.js diff --git a/www/Makefile b/www/Makefile index 38f9f3f37..3b2c9a27f 100644 --- a/www/Makefile +++ b/www/Makefile @@ -92,6 +92,7 @@ JSSRC= \ window/TrafficControlEdit.js \ window/ThresholdResetScheduleEdit.js \ window/MaxRetentionTimespanEdit.js \ + window/RetentionEdit.js \ window/NotifyOptions.js \ window/SyncJobEdit.js \ window/PruneJobEdit.js \ diff --git a/www/datastore/Content.js b/www/datastore/Content.js index bd989e660..dcb8ceaf6 100644 --- a/www/datastore/Content.js +++ b/www/datastore/Content.js @@ -46,6 +46,11 @@ Ext.define('pbs-data-store-snapshots', { type: 'boolean', defaultValue: true, }, + { + name: 'retain-until', + type: 'date', + dateFormat: 'timestamp', + }, ], }); @@ -842,6 +847,39 @@ Ext.define('PBS.DataStoreContent', { }); }, + onRetentionChange: function (table, rI, cI, item, e, rec) { + let me = this; + let view = me.getView(); + + if (!rec?.data || rec.data.ty !== 'dir' || !view.datastore) { + return; + } + if (Proxmox.UserName !== 'root@pam') { + return; + } + let data = rec.data; + + 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; + } + + Ext.create('PBS.window.RetentionEdit', { + subject: gettext('Retention') + ` - ${data.text}`, + autoShow: true, + datastore: view.datastore, + retainUntil: data['retain-until'], + extraRequestParams: params, + listeners: { + destroy: () => me.reload(), + }, + }); + }, + onForget: function (table, rI, cI, item, e, { data }) { let me = this; let view = this.getView(); @@ -1036,6 +1074,10 @@ Ext.define('PBS.DataStoreContent', { onCopy: createControllerCallback('onCopy'), onVerify: createControllerCallback('onVerify'), onProtectionChange: createControllerCallback('onProtectionChange'), + onRetentionChange: + Proxmox.UserName === 'root@pam' + ? createControllerCallback('onRetentionChange') + : undefined, onForget: createControllerCallback('onForget'), }); } @@ -1061,10 +1103,20 @@ Ext.define('PBS.DataStoreContent', { header: gettext('Backup Group'), dataIndex: 'text', renderer: (value, meta, record) => { + let attributes = []; if (record.data.protected) { - return `${value} (${gettext('protected')})`; + attributes.push(gettext('protected')); + } + let until = record.data['retain-until']; + if (until && until > new Date()) { + attributes.push( + Ext.String.format( + gettext('retained until {0}'), + Ext.Date.format(until, 'Y-m-d H:i:s'), + ), + ); } - return value; + return attributes.length ? `${value} (${attributes.join(', ')})` : value; }, flex: 1, }, @@ -1196,6 +1248,41 @@ Ext.define('PBS.DataStoreContent', { }, isActionDisabled: (v, r, c, i, rec) => rec.data.ty !== 'dir', }, + { + handler: 'onRetentionChange', + getTip: (v, m, rec) => { + if (rec.data.ty !== 'dir') { + return ''; + } + let isRoot = Proxmox.UserName === 'root@pam'; + let until = rec.data['retain-until']; + if (until && until > new Date()) { + let text = Ext.Date.format(until, 'Y-m-d H:i:s'); + return isRoot + ? Ext.String.format( + gettext('Retained until {0}, click to extend'), + text, + ) + : Ext.String.format(gettext('Retained until {0}'), text); + } + return isRoot ? gettext('Set retention') : ''; + }, + getClass: (v, m, rec) => { + if (rec.data.ty !== 'dir') { + return 'pmx-hidden'; + } + let until = rec.data['retain-until']; + if (until && until > new Date()) { + return 'fa fa-hourglass-half good'; + } + // only root can set or extend retention + return Proxmox.UserName === 'root@pam' + ? 'fa fa-hourglass-o faded' + : 'pmx-hidden'; + }, + isActionDisabled: (v, r, c, i, rec) => + rec.data.ty !== 'dir' || Proxmox.UserName !== 'root@pam', + }, { handler: 'onForget', getTip: (v, m, { data }) => { @@ -1603,6 +1690,7 @@ Ext.define('PBS.datastore.SnapshotCmdMenu', { onCopy: undefined, onVerify: undefined, onProtectionChange: undefined, + onRetentionChange: undefined, onForget: undefined, items: [ @@ -1639,6 +1727,17 @@ Ext.define('PBS.datastore.SnapshotCmdMenu', { disabled: '{!onProtectionChange}', }, }, + { + text: gettext('Set or Extend Retention'), + iconCls: 'fa fa-hourglass-half', + handler: function () { + this.up('menu').onRetentionChange(); + }, + cbind: { + hidden: '{!onRetentionChange}', + disabled: '{!onRetentionChange}', + }, + }, { xtype: 'menuseparator' }, { text: gettext('Remove'), diff --git a/www/window/RetentionEdit.js b/www/window/RetentionEdit.js new file mode 100644 index 000000000..4a6bdfa84 --- /dev/null +++ b/www/window/RetentionEdit.js @@ -0,0 +1,56 @@ +Ext.define('PBS.window.RetentionEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pbsRetentionEdit', + mixins: ['Proxmox.Mixin.CBind'], + + subject: gettext('Retention'), + width: 350, + + method: 'PUT', + isCreate: false, + autoLoad: false, + + datastore: undefined, + retainUntil: undefined, + + cbindData: function () { + let me = this; + + me.url = `/api2/extjs/admin/datastore/${encodeURIComponent(me.datastore)}/retention`; + + let now = new Date(); + return { + current: me.retainUntil, + minDateTime: me.retainUntil && me.retainUntil > now ? me.retainUntil : now, + }; + }, + + items: [ + { + xtype: 'displayfield', + fieldLabel: gettext('Retained Until'), + submitValue: false, + renderer: (val) => (val ? Ext.Date.format(val, 'Y-m-d H:i:s') : Proxmox.Utils.noneText), + cbind: { + value: '{current}', + }, + }, + { + xtype: 'proxmoxDateTimeField', + name: 'retain-until', + fieldLabel: gettext('New Retention'), + allowBlank: false, + cbind: { + minValue: '{minDateTime}', + value: '{current}', + }, + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext( + 'The retention can only be extended. Snapshots cannot be removed until the retention expires.', + ), + }, + ], +}); -- 2.47.3