diff --git a/www/Makefile b/www/Makefile index 79cb4c047..40111fd19 100644 --- a/www/Makefile +++ b/www/Makefile @@ -63,6 +63,7 @@ JSSRC= \ config/SyncView.js \ config/VerifyView.js \ config/PruneView.js \ + config/GCView.js \ config/WebauthnView.js \ config/CertificateView.js \ config/NodeOptionView.js \ @@ -79,6 +80,7 @@ JSSRC= \ window/NotifyOptions.js \ window/SyncJobEdit.js \ window/PruneJobEdit.js \ + window/GCJobEdit.js \ window/UserEdit.js \ window/Settings.js \ window/TokenEdit.js \ diff --git a/www/Utils.js b/www/Utils.js index 5357949b6..acd6e0d8d 100644 --- a/www/Utils.js +++ b/www/Utils.js @@ -199,12 +199,12 @@ Ext.define('PBS.Utils', { return fingerprint.substring(0, 23); }, - render_task_status: function(value, metadata, record) { - if (!record.data['last-run-upid']) { + render_task_status: function(value, metadata, record, rowIndex, colIndex, store) { + if (!record.data['last-run-upid'] && !store.getById('last-run-upid')?.data.value) { return '-'; } - if (!record.data['last-run-endtime']) { + if (!record.data['last-run-endtime'] && !store.getById('last-run-endtime')?.data.value) { metadata.tdCls = 'x-grid-row-loading'; return ''; } diff --git a/www/config/GCView.js b/www/config/GCView.js new file mode 100644 index 000000000..6c9e1e230 --- /dev/null +++ b/www/config/GCView.js @@ -0,0 +1,207 @@ +Ext.define('pbs-gc-jobs-status', { + extend: 'Ext.data.Model', + fields: [ + 'store', 'last-run-upid', 'removed-chunks', 'pending-chunks', 'schedule', + 'next-run', 'last-run-endtime', 'last-run-state', + { + name: 'duration', + calculate: function(data) { + let endtime = data['last-run-endtime']; + if (!endtime) return undefined; + let task = Proxmox.Utils.parse_task_upid(data['last-run-upid']); + return endtime - task.starttime; + }, + }, + ], + idProperty: 'store', + proxy: { + type: 'proxmox', + url: '/api2/json/admin/gc', + }, +}); + +Ext.define('PBS.config.GCJobView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pbsGCJobView', + + stateful: true, + stateId: 'grid-gc-jobs-v1', + allowDeselect: false, + + title: gettext('Garbage Collect Jobs'), + + controller: { + xclass: 'Ext.app.ViewController', + + init: function(view) { + let params = {}; + let store = view.getStore(); + let proxy = store.rstore.getProxy(); + if (view.datastore) { + params.store = view.datastore; + + // after the store is loaded, select the row to enable the Edit,.. buttons + store.rstore.proxy.on({ + 'afterload': { + fn: () => view.getSelectionModel().select(0), + single: true, + }, + }); + + // do not highlight the selected row + view.items.items[0].selectedItemCls = ''; + view.items.items[0].overItemCls = ''; + } + proxy.setExtraParams(params); + Proxmox.Utils.monStoreErrors(view, store.rstore); + }, + + getDatastoreName: function() { + return this.getView().getSelection()[0]?.data.store; + }, + + getData: function() { + let view = this.getView(); + let datastore = this.getDatastoreName(); + return view.getStore().getById(datastore).data; + }, + + editGCJob: function() { + let data = this.getData(); + Ext.create('PBS.window.GCJobEdit', { + datastore: data.store, + id: data.store, + schedule: data.schedule, + listeners: { + destroy: () => this.reload(), + }, + }).show(); + }, + + garbageCollect: function() { + let datastore = this.getDatastoreName(); + Proxmox.Utils.API2Request({ + url: `/admin/datastore/${datastore}/gc`, + method: 'POST', + failure: function(response) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + Ext.create('Proxmox.window.TaskViewer', { + upid: response.result.data, + }).show(); + }, + }); + }, + + showTaskLog: function() { + let me = this; + + let upid = this.getData()['last-run-upid']; + if (!upid) return; + + Ext.create('Proxmox.window.TaskViewer', { upid }).show(); + }, + + startStore: function() { this.getView().getStore().rstore.startUpdate(); }, + stopStore: function() { this.getView().getStore().rstore.stopUpdate(); }, + reload: function() { this.getView().getStore().rstore.load(); }, + + }, + + listeners: { + activate: 'startStore', + destroy: 'stopStore', + deactivate: 'stopStore', + itemdblclick: 'editGCJob', + }, + + store: { + type: 'diff', + autoDestroy: true, + autoDestroyRstore: true, + sorters: 'store', + rstore: { + type: 'update', + storeid: 'pbs-gc-jobs-status', + model: 'pbs-gc-jobs-status', + interval: 5000, + }, + }, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + handler: 'editGCJob', + enableFn: (rec) => !!rec, + disabled: true, + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Show Log'), + handler: 'showTaskLog', + enableFn: (rec) => !!rec.data["last-run-upid"], + disabled: true, + }, + { + xtype: 'proxmoxButton', + text: gettext('Run now'), + handler: 'garbageCollect', + enableFn: (rec) => !!rec, + disabled: true, + }, + ], + + columns: [ + { + header: gettext('Datastore'), + dataIndex: 'store', + renderer: Ext.String.htmlEncode, + width: 120, + sortable: true, + hideable: false, + }, + { + header: gettext('Schedule'), + dataIndex: 'schedule', + maxWidth: 220, + minWidth: 80, + flex: 1, + sortable: false, + hideable: false, + renderer: (value) => value ? value : Proxmox.Utils.NoneText, + }, + { + header: gettext('Last GC'), + dataIndex: 'last-run-endtime', + renderer: PBS.Utils.render_optional_timestamp, + minWidth: 150, + sortable: true, + }, + { + text: gettext('Duration'), + dataIndex: 'duration', + renderer: Proxmox.Utils.render_duration, + sortable: false, + width: 80, + }, + { + header: gettext('Last Status'), + dataIndex: 'last-run-state', + renderer: PBS.Utils.render_task_status, + sortable: true, + flex: 3, + maxWidth: 100, + minWidth: 80, + }, + { + header: gettext('Next Run'), + dataIndex: 'next-run', + renderer: PBS.Utils.render_next_task_run, + width: 150, + sortable: true, + }, + ], +}); diff --git a/www/datastore/DataStoreList.js b/www/datastore/DataStoreList.js index b496bcbc5..a4b77dbf6 100644 --- a/www/datastore/DataStoreList.js +++ b/www/datastore/DataStoreList.js @@ -239,8 +239,8 @@ Ext.define('PBS.datastore.DataStores', { }, { iconCls: 'fa fa-trash-o', - itemId: 'prunejobs', - xtype: 'pbsPruneJobView', + itemId: 'prunegc', + xtype: 'pbsDatastorePruneAndGC', }, { iconCls: 'fa fa-check-circle', diff --git a/www/datastore/Panel.js b/www/datastore/Panel.js index fd1b46113..0fc97d149 100644 --- a/www/datastore/Panel.js +++ b/www/datastore/Panel.js @@ -58,7 +58,6 @@ Ext.define('PBS.DataStorePanel', { }, }, { - title: gettext('Prune & GC'), xtype: 'pbsDatastorePruneAndGC', itemId: 'prunegc', iconCls: 'fa fa-trash-o', diff --git a/www/datastore/PruneAndGC.js b/www/datastore/PruneAndGC.js index aab98dadf..33ca01087 100644 --- a/www/datastore/PruneAndGC.js +++ b/www/datastore/PruneAndGC.js @@ -1,91 +1,8 @@ -Ext.define('PBS.Datastore.GCOptions', { - extend: 'Proxmox.grid.ObjectGrid', - alias: 'widget.pbsDatastoreGCOpts', - mixins: ['Proxmox.Mixin.CBind'], - - onlineHelp: 'maintenance_pruning', - - cbindData: function(initial) { - let me = this; - - me.datastore = encodeURIComponent(me.datastore); - me.url = `/api2/json/config/datastore/${me.datastore}`; - me.editorConfig = { - url: `/api2/extjs/config/datastore/${me.datastore}`, - }; - return {}; - }, - - controller: { - xclass: 'Ext.app.ViewController', - - edit: function() { this.getView().run_editor(); }, - - garbageCollect: function() { - let me = this; - let view = me.getView(); - Proxmox.Utils.API2Request({ - url: `/admin/datastore/${view.datastore}/gc`, - method: 'POST', - failure: function(response) { - Ext.Msg.alert(gettext('Error'), response.htmlStatus); - }, - success: function(response, options) { - Ext.create('Proxmox.window.TaskViewer', { - upid: response.result.data, - }).show(); - }, - }); - }, - }, - - tbar: [ - { - xtype: 'proxmoxButton', - text: gettext('Edit'), - disabled: true, - handler: 'edit', - }, - '-', - { - xtype: 'proxmoxButton', - text: gettext('Start Garbage Collection'), - selModel: null, - handler: 'garbageCollect', - }, - ], - - listeners: { - activate: function() { this.rstore.startUpdate(); }, - destroy: function() { this.rstore.stopUpdate(); }, - deactivate: function() { this.rstore.stopUpdate(); }, - itemdblclick: 'edit', - }, - - rows: { - "gc-schedule": { - required: true, - defaultValue: Proxmox.Utils.NoneText, - header: gettext('Garbage Collection Schedule'), - editor: { - xtype: 'proxmoxWindowEdit', - title: gettext('GC Schedule'), - onlineHelp: 'maintenance_gc', - items: { - xtype: 'pbsCalendarEvent', - name: 'gc-schedule', - fieldLabel: gettext("GC Schedule"), - emptyText: Proxmox.Utils.noneText, - deleteEmpty: true, - }, - }, - }, - }, -}); - Ext.define('PBS.Datastore.PruneAndGC', { extend: 'Ext.panel.Panel', alias: 'widget.pbsDatastorePruneAndGC', + title: gettext('Prune & GC Jobs'), + mixins: ['Proxmox.Mixin.CBind'], layout: { @@ -99,9 +16,8 @@ Ext.define('PBS.Datastore.PruneAndGC', { }, items: [ { - xtype: 'pbsDatastoreGCOpts', - title: gettext('Garbage Collection'), - itemId: 'datastore-gc', + xtype: 'pbsGCJobView', + itemId: 'gcjobs', nodename: 'localhost', cbind: { datastore: '{datastore}', @@ -110,9 +26,7 @@ Ext.define('PBS.Datastore.PruneAndGC', { { xtype: 'pbsPruneJobView', nodename: 'localhost', - itemId: 'datastore-prune-jobs', - flex: 1, - minHeight: 200, + itemId: 'prunejobs', cbind: { datastore: '{datastore}', }, @@ -130,4 +44,9 @@ Ext.define('PBS.Datastore.PruneAndGC', { component.relayEvents(me, ['activate', 'deactivate', 'destroy']); } }, + + cbindData: function(initalConfig) { + let me = this; + me.datastore = initalConfig.datastore ? initalConfig.datastore : undefined; + }, }); diff --git a/www/window/GCJobEdit.js b/www/window/GCJobEdit.js new file mode 100644 index 000000000..de74bf5cd --- /dev/null +++ b/www/window/GCJobEdit.js @@ -0,0 +1,28 @@ +Ext.define('PBS.window.GCJobEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pbsGCJobEdit', + mixins: ['Proxmox.Mixin.CBind'], + + userid: undefined, + onlineHelp: 'maintenance_gc', + isAdd: false, + + subject: gettext('Garbage Collect Schedule'), + + cbindData: function(initial) { + let me = this; + + me.datastore = encodeURIComponent(me.datastore); + me.url = `/api2/extjs/config/datastore/${me.datastore}`; + me.method = 'PUT'; + me.autoLoad = true; + return {}; + }, + + items: { + xtype: 'pbsCalendarEvent', + name: 'gc-schedule', + fieldLabel: gettext("GC Schedule"), + emptyText: gettext(Proxmox.Utils.NoneText + " (disabled)"), + }, +});