From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id B105C75721 for ; Thu, 24 Jun 2021 09:16:31 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 591BC12AAC for ; Thu, 24 Jun 2021 09:16:31 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 30BAA129FC for ; Thu, 24 Jun 2021 09:16:26 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 015A746786 for ; Thu, 24 Jun 2021 09:16:20 +0200 (CEST) From: Dominik Csapak To: pve-devel@lists.proxmox.com Date: Thu, 24 Jun 2021 09:16:16 +0200 Message-Id: <20210624071618.21507-2-d.csapak@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20210624071618.21507-1-d.csapak@proxmox.com> References: <20210624071618.21507-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.697 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH widget-toolkit 1/1] node/Tasks: merge improvements from PBS and make it more generic X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Thu, 24 Jun 2021 07:16:31 -0000 this copies most of the task grid from pbs, but adds handling so that users can add aribtrary filter fields the filter fields always present are: * since * until * task type * task status other filters fields can be added by giving an 'extraFilter' array which must contain widget definitions that emit a 'change' event. this is then used to update the filters for the api call also you can add a 'preFilter' object, that sets the filter parameter only once at the beginning Signed-off-by: Dominik Csapak --- src/css/ext6-pmx.css | 4 + src/node/Tasks.js | 578 ++++++++++++++++++++++++++++++------------- 2 files changed, 411 insertions(+), 171 deletions(-) diff --git a/src/css/ext6-pmx.css b/src/css/ext6-pmx.css index 7d7cddf..8e1980c 100644 --- a/src/css/ext6-pmx.css +++ b/src/css/ext6-pmx.css @@ -48,6 +48,10 @@ color: #FF6C59; } +.info-blue { + color: #3892d4; +} + /* reduce chart legend space usage to something more sane */ .x-legend-item { padding: 0.4em 0.8em 0.4em 1.8em; diff --git a/src/node/Tasks.js b/src/node/Tasks.js index 7f20a8a..049b527 100644 --- a/src/node/Tasks.js +++ b/src/node/Tasks.js @@ -1,217 +1,453 @@ Ext.define('Proxmox.node.Tasks', { extend: 'Ext.grid.GridPanel', - alias: ['widget.proxmoxNodeTasks'], + alias: 'widget.proxmoxNodeTasks', + stateful: true, - stateId: 'grid-node-tasks', + stateId: 'pve-grid-node-tasks', + loadMask: true, sortableColumns: false, - vmidFilter: 0, - initComponent: function() { - let me = this; + // set extra filter components, + // must have a 'name' property for the parameter, + // and must trigger a 'change' event + // if the value is 'undefined', it will not be sent to the api + extraFilter: [], - if (!me.nodename) { - throw "no node name specified"; - } - let store = Ext.create('Ext.data.BufferedStore', { - pageSize: 500, - autoLoad: true, - remoteFilter: true, - model: 'proxmox-tasks', - proxy: { - type: 'proxmox', - startParam: 'start', - limitParam: 'limit', - url: "/api2/json/nodes/" + me.nodename + "/tasks", - }, - }); + // filters that should only be set once and is not changable + // example: + // { + // vmid: 100, + // } + preFilter: {}, + + controller: { + xclass: 'Ext.app.ViewController', + + showTaskLog: function() { + let me = this; + let selection = me.getView().getSelection(); + if (selection.length < 1) { + return; + } - store.on('prefetch', function() { + let rec = selection[0]; + + Ext.create('Proxmox.window.TaskViewer', { + upid: rec.data.upid, + endtime: rec.data.endtime, + }).show(); + }, + + updateLayout: function() { + let me = this; // we want to update the scrollbar on every store load // since the total count might be different // the buffered grid plugin does this only on scrolling itself // and even reduces the scrollheight again when scrolling up - me.updateLayout(); - }); - - let userfilter = ''; - let filter_errors = 0; - - let updateProxyParams = function() { - let params = { - errors: filter_errors, - }; - if (userfilter) { - params.userfilter = userfilter; - } - if (me.vmidFilter) { - params.vmid = me.vmidFilter; - } - store.proxy.extraParams = params; - }; + me.getView().updateLayout(); + }, - updateProxyParams(); + sinceChange: function(field, newval) { + let me = this; + let vm = me.getViewModel(); - let reload_task = Ext.create('Ext.util.DelayedTask', function() { - updateProxyParams(); - store.reload(); - }); + vm.set('since', newval); + }, - let run_task_viewer = function() { - let sm = me.getSelectionModel(); - let rec = sm.getSelection()[0]; - if (!rec) { - return; - } + untilChange: function(field, newval, oldval) { + let me = this; + let vm = me.getViewModel(); + + vm.set('until', newval); + }, + + reload: function() { + let me = this; + let view = me.getView(); + view.getStore().load(); + }, + + showFilter: function(btn, pressed) { + let me = this; + let vm = me.getViewModel(); + vm.set('showFilter', pressed); + }, + + init: function(view) { + let me = this; + Proxmox.Utils.monStoreErrors(view, view.getStore(), true); + }, + }, - let win = Ext.create('Proxmox.window.TaskViewer', { - upid: rec.data.upid, - endtime: rec.data.endtime, - }); - win.show(); - }; - let view_btn = new Ext.Button({ - text: gettext('View'), - disabled: true, - handler: run_task_viewer, - }); - - Proxmox.Utils.monStoreErrors(me, store, true); - - Ext.apply(me, { - store: store, - viewConfig: { - trackOver: false, - stripeRows: false, // does not work with getRowClass() - - getRowClass: function(record, index) { - let status = record.get('status'); - - if (status) { - let parsed = Proxmox.Utils.parse_task_status(status); - if (parsed === 'error') { - return "proxmox-invalid-row"; - } else if (parsed === 'warning') { - return "proxmox-warning-row"; + listeners: { + itemdblclick: 'showTaskLog', + }, + + viewModel: { + data: { + typefilter: '', + statusfilter: '', + datastore: '', + showFilter: false, + extraFilter: {}, + since: null, + until: null, + }, + + formulas: { + filterIcon: (get) => 'fa fa-filter' + (get('showFilter') ? ' info-blue' : ''), + extraParams: function(get) { + let me = this; + let params = {}; + if (get('typefilter')) { + params.typefilter = get('typefilter'); + } + if (get('statusfilter')) { + params.statusfilter = get('statusfilter'); + } + if (get('datastore')) { + params.store = get('datastore'); + } + + if (get('extraFilter')) { + let extraFilter = get('extraFilter'); + for (const [name, value] of Object.entries(extraFilter)) { + if (value !== undefined && value !== null && value !== "") { + params[name] = value; } } - return ''; + } + + if (get('since')) { + params.since = get('since').valueOf()/1000; + } + + if (get('until')) { + let until = new Date(get('until').getTime()); // copy object + until.setDate(until.getDate() + 1); // end of the day + params.until = until.valueOf()/1000; + } + + me.getView().getStore().load(); + + return params; + }, + }, + + stores: { + bufferedstore: { + type: 'buffered', + pageSize: 500, + autoLoad: true, + remoteFilter: true, + model: 'proxmox-tasks', + proxy: { + type: 'proxmox', + startParam: 'start', + limitParam: 'limit', + extraParams: '{extraParams}', + url: "/api2/json/nodes/localhost/tasks", + }, + listeners: { + prefetch: 'updateLayout', }, }, - tbar: [ - view_btn, + }, + }, + + bind: { + store: '{bufferedstore}', + }, + + dockedItems: [ + { + xtype: 'toolbar', + items: [ { - text: gettext('Refresh'), // FIXME: smart-auto-refresh store - handler: () => store.reload(), + xtype: 'proxmoxButton', + text: gettext('View'), + iconCls: 'fa fa-window-restore', + disabled: true, + handler: 'showTaskLog', + }, + { + xtype: 'button', + text: gettext('Reload'), + iconCls: 'fa fa-refresh', + handler: 'reload', }, '->', - gettext('User name') +':', - ' ', { - xtype: 'textfield', - width: 200, - value: userfilter, - enableKeyEvents: true, - listeners: { - keyup: function(field, e) { - userfilter = field.getValue(); - reload_task.delay(500); - }, + xtype: 'button', + enableToggle: true, + bind: { + iconCls: '{filterIcon}', + }, + text: gettext('Filter'), + stateful: true, + stateId: 'task-showfilter', + stateEvents: ['toggle'], + applyState: function(state) { + if (state.pressed !== undefined) { + this.setPressed(state.pressed); + } + }, + getState: function() { + return { + pressed: this.pressed, + }; }, - }, ' ', gettext('Only Errors') + ':', ' ', - { - xtype: 'checkbox', - hideLabel: true, - checked: filter_errors, listeners: { - change: function(field, checked) { - filter_errors = checked ? 1 : 0; - reload_task.delay(10); - }, + toggle: 'showFilter', }, - }, ' ', + }, ], - columns: [ + }, + { + xtype: 'toolbar', + dock: 'top', + reference: 'filtertoolbar', + layout: { + type: 'hbox', + align: 'top', + }, + bind: { + hidden: '{!showFilter}', + }, + items: [ { - header: gettext("Start Time"), - dataIndex: 'starttime', - width: 130, - renderer: function(value) { - return Ext.Date.format(value, "M d H:i:s"); + xtype: 'container', + padding: 10, + layout: { + type: 'vbox', + align: 'stretch', }, - }, - { - header: gettext("End Time"), - dataIndex: 'endtime', - width: 130, - renderer: function(value, metaData, record) { - if (!value) { - metaData.tdCls = "x-grid-row-loading"; - return ''; - } - return Ext.Date.format(value, "M d H:i:s"); + defaults: { + labelWidth: 80, }, + // cannot bind the values directly, as it then changes also + // on blur, causing wrong reloads of the store + items: [ + { + xtype: 'datefield', + fieldLabel: gettext('Since'), + format: 'Y-m-d', + bind: { + maxValue: '{until}', + }, + listeners: { + change: 'sinceChange', + }, + }, + { + xtype: 'datefield', + fieldLabel: gettext('Until'), + format: 'Y-m-d', + bind: { + minValue: '{since}', + }, + listeners: { + change: 'untilChange', + }, + }, + ], }, { - header: gettext("Duration"), - hidden: true, - width: 80, - renderer: function(value, metaData, record) { - let start = record.data.starttime; - if (start) { - let end = record.data.endtime || Date.now(); - let duration = end - start; - if (duration > 0) { - duration /= 1000; - } - return Proxmox.Utils.format_duration_human(duration); - } - return Proxmox.Utils.unknownText; + xtype: 'container', + padding: 10, + layout: { + type: 'vbox', + align: 'stretch', }, + defaults: { + labelWidth: 80, + }, + items: [ + { + xtype: 'pmxTaskTypeSelector', + fieldLabel: gettext('Task Type'), + emptyText: gettext('All'), + bind: { + value: '{typefilter}', + }, + }, + { + xtype: 'combobox', + fieldLabel: gettext('Task Result'), + emptyText: gettext('All'), + multiSelect: true, + store: [ + ['ok', gettext('OK')], + ['unknown', Proxmox.Utils.unknownText], + ['warning', gettext('Warnings')], + ['error', gettext('Errors')], + ], + bind: { + value: '{statusfilter}', + }, + }, + ], }, - { - header: gettext("Node"), - dataIndex: 'node', - width: 120, - }, - { - header: gettext("User name"), - dataIndex: 'user', - width: 150, + ], + }, + ], + + viewConfig: { + trackOver: false, + stripeRows: false, // does not work with getRowClass() + emptyText: gettext('No Tasks found'), + + getRowClass: function(record, index) { + let status = record.get('status'); + + if (status) { + let parsed = Proxmox.Utils.parse_task_status(status); + if (parsed === 'error') { + return "proxmox-invalid-row"; + } else if (parsed === 'warning') { + return "proxmox-warning-row"; + } + } + return ''; + }, + }, + + columns: [ + { + header: gettext("Start Time"), + dataIndex: 'starttime', + width: 130, + renderer: function(value) { + return Ext.Date.format(value, "M d H:i:s"); + }, + }, + { + header: gettext("End Time"), + dataIndex: 'endtime', + width: 130, + renderer: function(value, metaData, record) { + if (!value) { + metaData.tdCls = "x-grid-row-loading"; + return ''; + } + return Ext.Date.format(value, "M d H:i:s"); + }, + }, + { + header: gettext("Duration"), + hidden: true, + width: 80, + renderer: function(value, metaData, record) { + let start = record.data.starttime; + if (start) { + let end = record.data.endtime || Date.now(); + let duration = end - start; + if (duration > 0) { + duration /= 1000; + } + return Proxmox.Utils.format_duration_human(duration); + } + return Proxmox.Utils.unknownText; + }, + }, + { + header: gettext("User name"), + dataIndex: 'user', + width: 150, + }, + { + header: gettext("Description"), + dataIndex: 'upid', + flex: 1, + renderer: Proxmox.Utils.render_upid, + }, + { + header: gettext("Status"), + dataIndex: 'status', + width: 200, + renderer: function(value, metaData, record) { + if (value === undefined && !record.data.endtime) { + metaData.tdCls = "x-grid-row-loading"; + return ''; + } + + let parsed = Proxmox.Utils.parse_task_status(value); + switch (parsed) { + case 'unknown': return Proxmox.Utils.unknownText; + case 'error': return Proxmox.Utils.errorText + ': ' + value; + case 'ok': // fall-through + case 'warning': // fall-through + default: return value; + } + }, + }, + ], + + initComponent: function() { + const me = this; + + let updateExtraFilters = function(name, value) { + let vm = me.getViewModel(); + let extraFilter = Ext.clone(vm.get('extraFilter')); + extraFilter[name] = value; + vm.set('extraFilter', extraFilter); + }; + + for (const [name, value] of Object.entries(me.preFilter)) { + updateExtraFilters(name, value); + } + + me.callParent(); + + let addFields = function(items) { + me.lookup('filtertoolbar').add({ + xtype: 'container', + padding: 10, + layout: { + type: 'vbox', + align: 'stretch', }, - { - header: gettext("Description"), - dataIndex: 'upid', - flex: 1, - renderer: Proxmox.Utils.render_upid, + defaults: { + labelWidth: 80, }, - { - header: gettext("Status"), - dataIndex: 'status', - width: 200, - renderer: function(value, metaData, record) { - if (value === undefined && !record.data.endtime) { - metaData.tdCls = "x-grid-row-loading"; - return ''; - } + items, + }); + }; - return Proxmox.Utils.format_task_status(value); - }, + // start with a userfilter + me.extraFilter = [ + { + xtype: 'textfield', + fieldLabel: gettext('User name'), + changeOptions: { + buffer: 500, }, - ], - listeners: { - itemdblclick: run_task_viewer, - selectionchange: function(v, selections) { - view_btn.setDisabled(!(selections && selections[0])); - }, - show: function() { reload_task.delay(10); }, - destroy: function() { reload_task.cancel(); }, + name: 'userfilter', }, - }); + ...me.extraFilter, + ]; + let items = []; + for (const filterTemplate of me.extraFilter) { + let filter = Ext.clone(filterTemplate); - me.callParent(); + filter.listeners = filter.listeners || {}; + filter.listeners.change = Ext.apply(filter.changeOptions || {}, { + fn: function(field, value) { + updateExtraFilters(filter.name, value); + }, + }); + + items.push(filter); + if (items.length === 2) { + addFields(items); + items = []; + } + } + + addFields(items); }, }); -- 2.20.1