]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/node/Tasks.js
fix #3542: node task logs: query correct node for tasks in clusters
[proxmox-widget-toolkit.git] / src / node / Tasks.js
1 Ext.define('Proxmox.node.Tasks', {
2 extend: 'Ext.grid.GridPanel',
3
4 alias: 'widget.proxmoxNodeTasks',
5
6 stateful: true,
7 stateId: 'pve-grid-node-tasks',
8
9 loadMask: true,
10 sortableColumns: false,
11
12 // set extra filter components, must have a 'name' property for the parameter, and must
13 // trigger a 'change' event if the value is 'undefined', it will not be sent to the api
14 extraFilter: [],
15
16
17 // fixed filters which cannot be changed after instantiation, for example:
18 // { vmid: 100 }
19 preFilter: {},
20
21 controller: {
22 xclass: 'Ext.app.ViewController',
23
24 showTaskLog: function() {
25 let me = this;
26 let selection = me.getView().getSelection();
27 if (selection.length < 1) {
28 return;
29 }
30
31 let rec = selection[0];
32
33 Ext.create('Proxmox.window.TaskViewer', {
34 upid: rec.data.upid,
35 endtime: rec.data.endtime,
36 }).show();
37 },
38
39 updateLayout: function(store, records, success, operation) {
40 let me = this;
41 let view = me.getView().getView(); // the table view, not the whole grid
42 Proxmox.Utils.setErrorMask(view, false);
43 // update the scrollbar on every store load since the total count might be different.
44 // the buffered grid plugin does this only on (user) scrolling itself and even reduces
45 // the scrollheight again when scrolling up
46 me.getView().updateLayout();
47
48 if (!success) {
49 Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
50 }
51 },
52
53 refresh: function() {
54 let me = this;
55 let view = me.getView();
56
57 let selection = view.getSelection();
58 let store = me.getViewModel().get('bufferedstore');
59 if (selection && selection.length > 0) {
60 // deselect if selection is not there anymore
61 if (!store.contains(selection[0])) {
62 view.setSelection(undefined);
63 }
64 }
65 },
66
67 sinceChange: function(field, newval) {
68 let me = this;
69 let vm = me.getViewModel();
70
71 vm.set('since', newval);
72 },
73
74 untilChange: function(field, newval, oldval) {
75 let me = this;
76 let vm = me.getViewModel();
77
78 vm.set('until', newval);
79 },
80
81 reload: function() {
82 let me = this;
83 let view = me.getView();
84 view.getStore().load();
85 },
86
87 showFilter: function(btn, pressed) {
88 let me = this;
89 let vm = me.getViewModel();
90 vm.set('showFilter', pressed);
91 },
92
93 clearFilter: function() {
94 let me = this;
95 me.lookup('filtertoolbar').query('field').forEach((field) => {
96 field.setValue(undefined);
97 });
98 },
99 },
100
101 listeners: {
102 itemdblclick: 'showTaskLog',
103 },
104
105 viewModel: {
106 data: {
107 typefilter: '',
108 statusfilter: '',
109 showFilter: false,
110 extraFilter: {},
111 since: null,
112 until: null,
113 },
114
115 formulas: {
116 filterIcon: (get) => 'fa fa-filter' + (get('showFilter') ? ' info-blue' : ''),
117 extraParams: function(get) {
118 let me = this;
119 let params = {};
120 if (get('typefilter')) {
121 params.typefilter = get('typefilter');
122 }
123 if (get('statusfilter')) {
124 params.statusfilter = get('statusfilter');
125 }
126
127 if (get('extraFilter')) {
128 let extraFilter = get('extraFilter');
129 for (const [name, value] of Object.entries(extraFilter)) {
130 if (value !== undefined && value !== null && value !== "") {
131 params[name] = value;
132 }
133 }
134 }
135
136 if (get('since')) {
137 params.since = get('since').valueOf()/1000;
138 }
139
140 if (get('until')) {
141 let until = new Date(get('until').getTime()); // copy object
142 until.setDate(until.getDate() + 1); // end of the day
143 params.until = until.valueOf()/1000;
144 }
145
146 me.getView().getStore().load();
147
148 return params;
149 },
150 filterCount: function(get) {
151 let count = 0;
152 if (get('typefilter')) {
153 count++;
154 }
155 let status = get('statusfilter');
156 if ((Ext.isArray(status) && status.length > 0) ||
157 (!Ext.isArray(status) && status)) {
158 count++;
159 }
160 if (get('since')) {
161 count++;
162 }
163 if (get('until')) {
164 count++;
165 }
166
167 if (get('extraFilter')) {
168 let extraFilter = get('extraFilter');
169 for (const value of Object.values(extraFilter)) {
170 if (value !== undefined && value !== null && value !== "") {
171 count++;
172 }
173 }
174 }
175
176 return count;
177 },
178 clearFilterText: function(get) {
179 let count = get('filterCount');
180 let fieldMsg = '';
181 if (count > 1) {
182 fieldMsg = ` (${count} ${gettext('Fields')})`;
183 } else if (count > 0) {
184 fieldMsg = ` (1 ${gettext('Field')})`;
185 }
186 return gettext('Clear Filter') + fieldMsg;
187 },
188 },
189
190 stores: {
191 bufferedstore: {
192 type: 'buffered',
193 pageSize: 500,
194 autoLoad: true,
195 remoteFilter: true,
196 model: 'proxmox-tasks',
197 proxy: {
198 type: 'proxmox',
199 startParam: 'start',
200 limitParam: 'limit',
201 extraParams: '{extraParams}',
202 url: '{url}',
203 },
204 listeners: {
205 prefetch: 'updateLayout',
206 refresh: 'refresh',
207 },
208 },
209 },
210 },
211
212 bind: {
213 store: '{bufferedstore}',
214 },
215
216 dockedItems: [
217 {
218 xtype: 'toolbar',
219 items: [
220 {
221 xtype: 'proxmoxButton',
222 text: gettext('View'),
223 iconCls: 'fa fa-window-restore',
224 disabled: true,
225 handler: 'showTaskLog',
226 },
227 {
228 xtype: 'button',
229 text: gettext('Reload'),
230 iconCls: 'fa fa-refresh',
231 handler: 'reload',
232 },
233 '->',
234 {
235 xtype: 'button',
236 bind: {
237 text: '{clearFilterText}',
238 disabled: '{!filterCount}',
239 },
240 text: gettext('Clear Filter'),
241 enabled: false,
242 handler: 'clearFilter',
243 },
244 {
245 xtype: 'button',
246 enableToggle: true,
247 bind: {
248 iconCls: '{filterIcon}',
249 },
250 text: gettext('Filter'),
251 stateful: true,
252 stateId: 'task-showfilter',
253 stateEvents: ['toggle'],
254 applyState: function(state) {
255 if (state.pressed !== undefined) {
256 this.setPressed(state.pressed);
257 }
258 },
259 getState: function() {
260 return {
261 pressed: this.pressed,
262 };
263 },
264 listeners: {
265 toggle: 'showFilter',
266 },
267 },
268 ],
269 },
270 {
271 xtype: 'toolbar',
272 dock: 'top',
273 reference: 'filtertoolbar',
274 layout: {
275 type: 'hbox',
276 align: 'top',
277 },
278 bind: {
279 hidden: '{!showFilter}',
280 },
281 items: [
282 {
283 xtype: 'container',
284 padding: 10,
285 layout: {
286 type: 'vbox',
287 align: 'stretch',
288 },
289 defaults: {
290 labelWidth: 80,
291 },
292 // cannot bind the values directly, as it then changes also
293 // on blur, causing wrong reloads of the store
294 items: [
295 {
296 xtype: 'datefield',
297 fieldLabel: gettext('Since'),
298 format: 'Y-m-d',
299 bind: {
300 maxValue: '{until}',
301 },
302 listeners: {
303 change: 'sinceChange',
304 },
305 },
306 {
307 xtype: 'datefield',
308 fieldLabel: gettext('Until'),
309 format: 'Y-m-d',
310 bind: {
311 minValue: '{since}',
312 },
313 listeners: {
314 change: 'untilChange',
315 },
316 },
317 ],
318 },
319 {
320 xtype: 'container',
321 padding: 10,
322 layout: {
323 type: 'vbox',
324 align: 'stretch',
325 },
326 defaults: {
327 labelWidth: 80,
328 },
329 items: [
330 {
331 xtype: 'pmxTaskTypeSelector',
332 fieldLabel: gettext('Task Type'),
333 emptyText: gettext('All'),
334 bind: {
335 value: '{typefilter}',
336 },
337 },
338 {
339 xtype: 'combobox',
340 fieldLabel: gettext('Task Result'),
341 emptyText: gettext('All'),
342 multiSelect: true,
343 store: [
344 ['ok', gettext('OK')],
345 ['unknown', Proxmox.Utils.unknownText],
346 ['warning', gettext('Warnings')],
347 ['error', gettext('Errors')],
348 ],
349 bind: {
350 value: '{statusfilter}',
351 },
352 },
353 ],
354 },
355 ],
356 },
357 ],
358
359 viewConfig: {
360 trackOver: false,
361 stripeRows: false, // does not work with getRowClass()
362 emptyText: gettext('No Tasks found'),
363
364 getRowClass: function(record, index) {
365 let status = record.get('status');
366
367 if (status) {
368 let parsed = Proxmox.Utils.parse_task_status(status);
369 if (parsed === 'error') {
370 return "proxmox-invalid-row";
371 } else if (parsed === 'warning') {
372 return "proxmox-warning-row";
373 }
374 }
375 return '';
376 },
377 },
378
379 columns: [
380 {
381 header: gettext("Start Time"),
382 dataIndex: 'starttime',
383 width: 130,
384 renderer: function(value) {
385 return Ext.Date.format(value, "M d H:i:s");
386 },
387 },
388 {
389 header: gettext("End Time"),
390 dataIndex: 'endtime',
391 width: 130,
392 renderer: function(value, metaData, record) {
393 if (!value) {
394 metaData.tdCls = "x-grid-row-loading";
395 return '';
396 }
397 return Ext.Date.format(value, "M d H:i:s");
398 },
399 },
400 {
401 header: gettext("Duration"),
402 hidden: true,
403 width: 80,
404 renderer: function(value, metaData, record) {
405 let start = record.data.starttime;
406 if (start) {
407 let end = record.data.endtime || Date.now();
408 let duration = end - start;
409 if (duration > 0) {
410 duration /= 1000;
411 }
412 return Proxmox.Utils.format_duration_human(duration);
413 }
414 return Proxmox.Utils.unknownText;
415 },
416 },
417 {
418 header: gettext("User name"),
419 dataIndex: 'user',
420 width: 150,
421 },
422 {
423 header: gettext("Description"),
424 dataIndex: 'upid',
425 flex: 1,
426 renderer: Proxmox.Utils.render_upid,
427 },
428 {
429 header: gettext("Status"),
430 dataIndex: 'status',
431 width: 200,
432 renderer: function(value, metaData, record) {
433 if (value === undefined && !record.data.endtime) {
434 metaData.tdCls = "x-grid-row-loading";
435 return '';
436 }
437
438 return Proxmox.Utils.format_task_status(value);
439 },
440 },
441 ],
442
443 initComponent: function() {
444 const me = this;
445
446 let nodename = me.nodename || 'localhost';
447 let url = me.url || `/api2/json/nodes/${nodename}/tasks`;
448 me.getViewModel().set('url', url);
449
450 let updateExtraFilters = function(name, value) {
451 let vm = me.getViewModel();
452 let extraFilter = Ext.clone(vm.get('extraFilter'));
453 extraFilter[name] = value;
454 vm.set('extraFilter', extraFilter);
455 };
456
457 for (const [name, value] of Object.entries(me.preFilter)) {
458 updateExtraFilters(name, value);
459 }
460
461 me.callParent();
462
463 let addFields = function(items) {
464 me.lookup('filtertoolbar').add({
465 xtype: 'container',
466 padding: 10,
467 layout: {
468 type: 'vbox',
469 align: 'stretch',
470 },
471 defaults: {
472 labelWidth: 80,
473 },
474 items,
475 });
476 };
477
478 // start with a userfilter
479 me.extraFilter = [
480 {
481 xtype: 'textfield',
482 fieldLabel: gettext('User name'),
483 changeOptions: {
484 buffer: 500,
485 },
486 name: 'userfilter',
487 },
488 ...me.extraFilter,
489 ];
490 let items = [];
491 for (const filterTemplate of me.extraFilter) {
492 let filter = Ext.clone(filterTemplate);
493
494 filter.listeners = filter.listeners || {};
495 filter.listeners.change = Ext.apply(filter.changeOptions || {}, {
496 fn: function(field, value) {
497 updateExtraFilters(filter.name, value);
498 },
499 });
500
501 items.push(filter);
502 if (items.length === 2) {
503 addFields(items);
504 items = [];
505 }
506 }
507
508 addFields(items);
509 },
510 });