* [pve-devel] [PATCH widget-toolkit 1/1] node/Tasks: merge improvements from PBS and make it more generic
2021-06-24 7:16 [pve-devel] [PATCH widget-toolkit/manager] unify gui for task listing Dominik Csapak
@ 2021-06-24 7:16 ` Dominik Csapak
2021-06-28 15:47 ` [pve-devel] applied: " Thomas Lamprecht
2021-06-24 7:16 ` [pve-devel] [PATCH manager 1/2] {qemu, lxc}/Config: adapt to new proxmoxNodeTasks Dominik Csapak
` (2 subsequent siblings)
3 siblings, 1 reply; 9+ messages in thread
From: Dominik Csapak @ 2021-06-24 7:16 UTC (permalink / raw)
To: pve-devel
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 <d.csapak@proxmox.com>
---
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
^ permalink raw reply [flat|nested] 9+ messages in thread