[pve-devel] [PATCH widget-toolkit 1/1] node/Tasks: merge improvements from PBS and make it more generic

Dominik Csapak d.csapak at proxmox.com
Thu Jun 24 09:16:16 CEST 2021


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 at 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






More information about the pve-devel mailing list