diff --git a/www/Makefile b/www/Makefile index 60af9ca3..311f4753 100644 --- a/www/Makefile +++ b/www/Makefile @@ -58,6 +58,7 @@ JSSRC= \ config/ACLView.js \ config/SyncView.js \ config/VerifyView.js \ + config/PruneView.js \ config/WebauthnView.js \ config/CertificateView.js \ config/NodeOptionView.js \ @@ -73,6 +74,7 @@ JSSRC= \ window/TrafficControlEdit.js \ window/NotifyOptions.js \ window/SyncJobEdit.js \ + window/PruneJobEdit.js \ window/UserEdit.js \ window/UserPassword.js \ window/Settings.js \ diff --git a/www/config/PruneView.js b/www/config/PruneView.js new file mode 100644 index 00000000..b3bcd7fb --- /dev/null +++ b/www/config/PruneView.js @@ -0,0 +1,285 @@ +Ext.define('pbs-prune-jobs-status', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'disable', 'store', 'ns', 'max-depth', 'schedule', + 'keep-last', 'keep-hourly', 'keep-daily', 'keep-weekly', 'keep-monthly', 'keep-yearly', + 'next-run', 'last-run-upid', 'last-run-state', 'last-run-endtime', + { + 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; + }, + }, + 'comment', + ], + idProperty: 'id', + proxy: { + type: 'proxmox', + url: '/api2/json/admin/prune', + }, +}); + +Ext.define('PBS.config.PruneJobView', { + extend: 'Ext.grid.GridPanel', + alias: 'widget.pbsPruneJobView', + + stateful: true, + stateId: 'grid-prune-jobs-v1', + + title: gettext('Prune Jobs'), + + controller: { + xclass: 'Ext.app.ViewController', + + addPruneJob: function() { + let me = this; + let view = me.getView(); + Ext.create('PBS.window.PruneJobEdit', { + datastore: view.datastore, + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + editPruneJob: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection.length < 1) return; + + Ext.create('PBS.window.PruneJobEdit', { + datastore: view.datastore, + id: selection[0].data.id, + listeners: { + destroy: function() { + me.reload(); + }, + }, + }).show(); + }, + + openTaskLog: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection.length < 1) return; + + let upid = selection[0].data['last-run-upid']; + if (!upid) return; + + Ext.create('Proxmox.window.TaskViewer', { + upid, + }).show(); + }, + + runPruneJob: function() { + let me = this; + let view = me.getView(); + let selection = view.getSelection(); + if (selection.length < 1) return; + + let id = selection[0].data.id; + Proxmox.Utils.API2Request({ + method: 'POST', + url: `/admin/prune/${id}/run`, + success: function(response, opt) { + Ext.create('Proxmox.window.TaskViewer', { + upid: response.result.data, + taskDone: function(success) { + me.reload(); + }, + }).show(); + }, + failure: function(response, opt) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + }); + }, + + render_optional_owner: function(value, metadata, record) { + if (!value) return '-'; + return Ext.String.htmlEncode(value); + }, + + startStore: function() { this.getView().getStore().rstore.startUpdate(); }, + stopStore: function() { this.getView().getStore().rstore.stopUpdate(); }, + + reload: function() { this.getView().getStore().rstore.load(); }, + + init: function(view) { + let params = {}; + if (view.datastore !== undefined) { + params.store = view.datastore; + } + view.getStore().rstore.getProxy().setExtraParams(params); + Proxmox.Utils.monStoreErrors(view, view.getStore().rstore); + }, + }, + + listeners: { + activate: 'startStore', + deactivate: 'stopStore', + itemdblclick: 'editPruneJob', + }, + + store: { + type: 'diff', + autoDestroy: true, + autoDestroyRstore: true, + sorters: 'id', + rstore: { + type: 'update', + storeid: 'pbs-prune-jobs-status', + model: 'pbs-prune-jobs-status', + interval: 5000, + }, + }, + + tbar: [ + { + xtype: 'proxmoxButton', + text: gettext('Add'), + handler: 'addPruneJob', + selModel: false, + }, + { + xtype: 'proxmoxButton', + text: gettext('Edit'), + handler: 'editPruneJob', + disabled: true, + }, + { + xtype: 'proxmoxStdRemoveButton', + baseurl: '/config/prune/', + confirmMsg: gettext('Remove entry?'), + callback: 'reload', + }, + '-', + { + xtype: 'proxmoxButton', + text: gettext('Show Log'), + handler: 'openTaskLog', + enableFn: (rec) => !!rec.data['last-run-upid'], + disabled: true, + }, + { + xtype: 'proxmoxButton', + text: gettext('Run now'), + handler: 'runPruneJob', + disabled: true, + }, + ], + + viewConfig: { + trackOver: false, + }, + + columns: [ + { + header: gettext('Job ID'), + dataIndex: 'id', + renderer: Ext.String.htmlEncode, + maxWidth: 220, + minWidth: 75, + flex: 1, + sortable: true, + }, + { + header: gettext('Store'), + dataIndex: 'store', + width: 120, + sortable: true, + }, + { + header: gettext('Namespace'), + dataIndex: 'ns', + width: 120, + sortable: true, + renderer: PBS.Utils.render_optional_namespace, + }, + { + header: gettext('Max. Recursion'), + dataIndex: 'max-depth', + width: 40, + sortable: true, + }, + { + header: gettext('Schedule'), + dataIndex: 'schedule', + maxWidth: 220, + minWidth: 80, + flex: 1, + sortable: true, + }, + { + text: gettext('Keep'), + defaults: { + width: 60, + }, + columns: [ + ['last', gettext('Last')], + ['hourly', gettext('Hourly')], + ['daily', gettext('Daily')], + ['weekly', gettext('Weekly')], + ['monthly', gettext('Monthly')], + ['yearly', gettext('Yearly')], + ].map(([data, header]) => ({ + header: header, + dataIndex: `keep-${data}`, + })), + }, + { + header: gettext('Last Prune'), + dataIndex: 'last-run-endtime', + renderer: PBS.Utils.render_optional_timestamp, + width: 150, + sortable: true, + }, + { + text: gettext('Duration'), + dataIndex: 'duration', + renderer: Proxmox.Utils.render_duration, + width: 80, + }, + { + header: gettext('Status'), + dataIndex: 'last-run-state', + renderer: PBS.Utils.render_task_status, + flex: 3, + }, + { + header: gettext('Next Run'), + dataIndex: 'next-run', + renderer: PBS.Utils.render_next_task_run, + width: 150, + sortable: true, + }, + { + header: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.String.htmlEncode, + flex: 2, + sortable: true, + }, + ], + + initComponent: function() { + let me = this; + let hideLocalDatastore = !!me.datastore; + + for (let column of me.columns) { + if (column.dataIndex === 'store') { + column.hidden = hideLocalDatastore; + break; + } + } + + me.callParent(); + }, +}); diff --git a/www/datastore/DataStoreList.js b/www/datastore/DataStoreList.js index 353709d3..b496bcbc 100644 --- a/www/datastore/DataStoreList.js +++ b/www/datastore/DataStoreList.js @@ -41,6 +41,10 @@ Ext.define('PBS.datastore.DataStoreList', { type = 'verify'; } + if (type === 'prunejob') { + type = 'prune'; + } + let datastore = PBS.Utils.parse_datastore_worker_id(type, task.worker_id); if (!datastore) { return; @@ -233,6 +237,11 @@ Ext.define('PBS.datastore.DataStores', { itemId: 'syncjobs', xtype: 'pbsSyncJobView', }, + { + iconCls: 'fa fa-trash-o', + itemId: 'prunejobs', + xtype: 'pbsPruneJobView', + }, { iconCls: 'fa fa-check-circle', itemId: 'verifyjobs', diff --git a/www/datastore/Panel.js b/www/datastore/Panel.js index fdb457a4..032d5e01 100644 --- a/www/datastore/Panel.js +++ b/www/datastore/Panel.js @@ -74,6 +74,14 @@ Ext.define('PBS.DataStorePanel', { datastore: '{datastore}', }, }, + { + iconCls: 'fa fa-trash-o', + itemId: 'prunejobs', + xtype: 'pbsPruneJobView', + cbind: { + datastore: '{datastore}', + }, + }, { iconCls: 'fa fa-check-circle', itemId: 'verifyjobs', diff --git a/www/window/NotifyOptions.js b/www/window/NotifyOptions.js index 2312988b..924bbb8b 100644 --- a/www/window/NotifyOptions.js +++ b/www/window/NotifyOptions.js @@ -72,6 +72,13 @@ Ext.define('PBS.window.NotifyOptions', { value: '__default__', deleteEmpty: false, }, + { + xtype: 'pbsNotifyType', + name: 'prune', + fieldLabel: gettext('Prune Jobs'), + value: '__default__', + deleteEmpty: false, + }, { xtype: 'pbsNotifyType', name: 'gc', diff --git a/www/window/PruneJobEdit.js b/www/window/PruneJobEdit.js new file mode 100644 index 00000000..66847a93 --- /dev/null +++ b/www/window/PruneJobEdit.js @@ -0,0 +1,196 @@ +Ext.define('PBS.window.PruneJobEdit', { + extend: 'Proxmox.window.Edit', + alias: 'widget.pbsPruneJobEdit', + mixins: ['Proxmox.Mixin.CBind'], + + userid: undefined, + + onlineHelp: 'prunejobs', + + isAdd: true, + + subject: gettext('Prune Job'), + + bodyPadding: 0, + + fieldDefaults: { labelWidth: 120 }, + defaultFocus: 'proxmoxtextfield[name=comment]', + + cbindData: function(initialConfig) { + let me = this; + + let baseurl = '/api2/extjs/config/prune'; + let id = initialConfig.id; + + me.isCreate = !id; + me.url = id ? `${baseurl}/${id}` : baseurl; + me.method = id ? 'PUT' : 'POST'; + me.autoLoad = !!id; + me.scheduleValue = id ? null : 'hourly'; + me.authid = id ? null : Proxmox.UserName; + me.editDatastore = me.datastore === undefined && me.isCreate; + return { }; + }, + + controller: { + xclass: 'Ext.app.ViewController', + control: { + 'pbsDataStoreSelector[name=store]': { + change: 'storeChange', + }, + }, + + storeChange: function(field, value) { + let view = this.getView(); + let nsSelector = view.down('pbsNamespaceSelector[name=ns]'); + nsSelector.setDatastore(value); + }, + }, + + + items: { + xtype: 'tabpanel', + bodyPadding: 10, + border: 0, + items: [ + { + title: 'Options', + xtype: 'inputpanel', + onGetValues: function(values) { + let me = this; + + if (!values.id && me.up('pbsPruneJobEdit').isCreate) { + values.id = 's-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13); + } + if (!me.isCreate) { + if (typeof values.delete === 'string') { + values.delete = values.delete.split(','); + } + } + return values; + }, + column1: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Datastore'), + name: 'store', + submitValue: true, + cbind: { + editable: '{editDatastore}', + value: '{datastore}', + }, + editConfig: { + xtype: 'pbsDataStoreSelector', + allowBlank: false, + }, + }, + { + xtype: 'pbsNamespaceSelector', + fieldLabel: gettext('Namespace'), + name: 'ns', + cbind: { + datastore: '{datastore}', + }, + listeners: { + change: function(field, localNs) { + let me = this; + let view = me.up('pbsPruneJobEdit'); + + let maxDepthField = view.down('field[name=max-depth]'); + maxDepthField.setLimit(localNs); + maxDepthField.validate(); + }, + }, + }, + { + fieldLabel: gettext('Prune Schedule'), + xtype: 'pbsCalendarEvent', + name: 'schedule', + emptyText: gettext('none (disabled)'), + cbind: { + deleteEmpty: '{!isCreate}', + value: '{scheduleValue}', + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Enabled'), + name: 'enable', + uncheckedValue: 0, + defaultValue: 1, + checked: true, + }, + ], + + column2: [ + { + xtype: 'pbsNamespaceMaxDepthReduced', + name: 'max-depth', + fieldLabel: gettext('Max. Depth'), + deleteEmpty: true, + }, + { + xtype: 'pbsPruneKeepInput', + name: 'keep-last', + fieldLabel: gettext('Keep Last'), + deleteEmpty: true, + }, + { + xtype: 'pbsPruneKeepInput', + name: 'keep-hourly', + fieldLabel: gettext('Keep Hourly'), + deleteEmpty: true, + }, + { + xtype: 'pbsPruneKeepInput', + name: 'keep-daily', + fieldLabel: gettext('Keep Daily'), + deleteEmpty: true, + }, + { + xtype: 'pbsPruneKeepInput', + name: 'keep-weekly', + fieldLabel: gettext('Keep Weekly'), + deleteEmpty: true, + }, + { + xtype: 'pbsPruneKeepInput', + name: 'keep-monthly', + fieldLabel: gettext('Keep Monthly'), + deleteEmpty: true, + }, + { + xtype: 'pbsPruneKeepInput', + name: 'keep-yearly', + fieldLabel: gettext('Keep Yearly'), + deleteEmpty: true, + }, + ], + + columnB: [ + { + fieldLabel: gettext('Comment'), + xtype: 'proxmoxtextfield', + name: 'comment', + cbind: { + deleteEmpty: '{!isCreate}', + }, + }, + ], + advancedColumn1: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Job ID'), + emptyText: gettext('Autogenerate'), + name: 'id', + allowBlank: true, + regex: PBS.Utils.SAFE_ID_RE, + cbind: { + editable: '{isCreate}', + }, + }, + ], + }, + ], + }, +});