]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/window/BulkAction.js
ui: bulk actions: add clear filters button
[pve-manager.git] / www / manager6 / window / BulkAction.js
1 Ext.define('PVE.window.BulkAction', {
2 extend: 'Ext.window.Window',
3
4 resizable: true,
5 width: 800,
6 height: 600,
7 modal: true,
8 layout: {
9 type: 'fit',
10 },
11 border: false,
12
13 // the action to set, currently there are: `startall`, `migrateall`, `stopall`
14 action: undefined,
15
16 submit: function(params) {
17 let me = this;
18
19 Proxmox.Utils.API2Request({
20 params: params,
21 url: `/nodes/${me.nodename}/${me.action}`,
22 waitMsgTarget: me,
23 method: 'POST',
24 failure: response => Ext.Msg.alert('Error', response.htmlStatus),
25 success: function({ result }, options) {
26 Ext.create('Proxmox.window.TaskViewer', {
27 autoShow: true,
28 upid: result.data,
29 listeners: {
30 destroy: () => me.close(),
31 },
32 });
33 me.hide();
34 },
35 });
36 },
37
38 initComponent: function() {
39 let me = this;
40
41 if (!me.nodename) {
42 throw "no node name specified";
43 }
44 if (!me.action) {
45 throw "no action specified";
46 }
47 if (!me.btnText) {
48 throw "no button text specified";
49 }
50 if (!me.title) {
51 throw "no title specified";
52 }
53
54 let items = [];
55 if (me.action === 'migrateall') {
56 items.push(
57 {
58 xtype: 'fieldcontainer',
59 layout: 'hbox',
60 items: [{
61 flex: 1,
62 xtype: 'pveNodeSelector',
63 name: 'target',
64 disallowedNodes: [me.nodename],
65 fieldLabel: gettext('Target node'),
66 labelWidth: 200,
67 allowBlank: false,
68 onlineValidator: true,
69 padding: '0 10 0 0',
70 },
71 {
72 xtype: 'proxmoxintegerfield',
73 name: 'maxworkers',
74 minValue: 1,
75 maxValue: 100,
76 value: 1,
77 fieldLabel: gettext('Parallel jobs'),
78 allowBlank: false,
79 flex: 1,
80 }],
81 },
82 {
83 xtype: 'fieldcontainer',
84 layout: 'hbox',
85 items: [{
86 xtype: 'proxmoxcheckbox',
87 fieldLabel: gettext('Allow local disk migration'),
88 name: 'with-local-disks',
89 labelWidth: 200,
90 checked: true,
91 uncheckedValue: 0,
92 flex: 1,
93 padding: '0 10 0 0',
94 },
95 {
96 itemId: 'lxcwarning',
97 xtype: 'displayfield',
98 userCls: 'pmx-hint',
99 value: 'Warning: Running CTs will be migrated in Restart Mode.',
100 hidden: true, // only visible if running container chosen
101 flex: 1,
102 }],
103 },
104 );
105 } else if (me.action === 'startall') {
106 items.push({
107 xtype: 'hiddenfield',
108 name: 'force',
109 value: 1,
110 });
111 } else if (me.action === 'stopall') {
112 items.push({
113 xtype: 'fieldcontainer',
114 layout: 'hbox',
115 items: [{
116 xtype: 'proxmoxcheckbox',
117 name: 'force-stop',
118 labelWidth: 120,
119 fieldLabel: gettext('Force Stop'),
120 boxLabel: gettext('Force stop guest if shutdown times out.'),
121 checked: true,
122 uncheckedValue: 0,
123 flex: 1,
124 },
125 {
126 xtype: 'proxmoxintegerfield',
127 name: 'timeout',
128 fieldLabel: gettext('Timeout (s)'),
129 labelWidth: 120,
130 emptyText: '180',
131 minValue: 0,
132 maxValue: 7200,
133 allowBlank: true,
134 flex: 1,
135 }],
136 });
137 }
138
139 let refreshLxcWarning = function(vmids, records) {
140 let showWarning = records.some(
141 item => vmids.includes(item.data.vmid) && item.data.type === 'lxc' && item.data.status === 'running',
142 );
143 me.down('#lxcwarning').setVisible(showWarning);
144 };
145
146 let defaultStatus = me.action === 'migrateall' ? '' : me.action === 'startall' ? 'stopped' : 'running';
147
148 let statusMap = [];
149 let poolMap = [];
150 let haMap = [];
151 let tagMap = [];
152 PVE.data.ResourceStore.each((rec) => {
153 if (['qemu', 'lxc'].indexOf(rec.data.type) !== -1) {
154 statusMap[rec.data.status] = true;
155 }
156 if (rec.data.type === 'pool') {
157 poolMap[rec.data.pool] = true;
158 }
159 if (rec.data.hastate !== "") {
160 haMap[rec.data.hastate] = true;
161 }
162 if (rec.data.tags !== "") {
163 rec.data.tags.split(/[,; ]/).forEach((tag) => {
164 if (tag !== '') {
165 tagMap[tag] = true;
166 }
167 });
168 }
169 });
170
171 let statusList = Object.keys(statusMap).map(key => [key, key]);
172 statusList.unshift(['', gettext('All')]);
173 let poolList = Object.keys(poolMap).map(key => [key, key]);
174 let tagList = Object.keys(tagMap).map(key => ({ value: key }));
175 let haList = Object.keys(haMap).map(key => [key, key]);
176
177 let clearFilters = function() {
178 me.down('#namefilter').setValue('');
179 ['name', 'status', 'pool', 'type', 'hastate', 'includetag', 'excludetag', 'vmid'].forEach((filter) => {
180 me.down(`#${filter}filter`).setValue('');
181 });
182 };
183
184 let filterChange = function() {
185 let nameValue = me.down('#namefilter').getValue();
186 let filterCount = 0;
187
188 if (nameValue !== '') {
189 filterCount++;
190 }
191
192 let arrayFiltersData = [];
193 ['pool', 'hastate'].forEach((filter) => {
194 let selected = me.down(`#${filter}filter`).getValue() ?? [];
195 if (selected.length) {
196 filterCount++;
197 arrayFiltersData.push([filter, [...selected]]);
198 }
199 });
200
201 let singleFiltersData = [];
202 ['status', 'type'].forEach((filter) => {
203 let selected = me.down(`#${filter}filter`).getValue() ?? '';
204 if (selected.length) {
205 filterCount++;
206 singleFiltersData.push([filter, selected]);
207 }
208 });
209
210 let includeTags = me.down('#includetagfilter').getValue() ?? [];
211 if (includeTags.length) {
212 filterCount++;
213 }
214 let excludeTags = me.down('#excludetagfilter').getValue() ?? [];
215 if (excludeTags.length) {
216 filterCount++;
217 }
218
219 let fieldSet = me.down('#filters');
220 let clearBtn = me.down('#clearBtn');
221 if (filterCount) {
222 fieldSet.setTitle(Ext.String.format(gettext('Filters ({0})'), filterCount));
223 clearBtn.setDisabled(false);
224 } else {
225 fieldSet.setTitle(gettext('Filters'));
226 clearBtn.setDisabled(true);
227 }
228
229 let filterFn = function(value) {
230 let name = value.data.name.toLowerCase().indexOf(nameValue.toLowerCase()) !== -1;
231 let arrayFilters = arrayFiltersData.every(([filter, selected]) =>
232 !selected.length || selected.indexOf(value.data[filter]) !== -1);
233 let singleFilters = singleFiltersData.every(([filter, selected]) =>
234 !selected.length || value.data[filter].indexOf(selected) !== -1);
235 let tags = value.data.tags.split(/[;, ]/).filter(t => !!t);
236 let includeFilter = !includeTags.length || tags.some(tag => includeTags.indexOf(tag) !== -1);
237 let excludeFilter = !excludeTags.length || tags.every(tag => excludeTags.indexOf(tag) === -1);
238
239 return name && arrayFilters && singleFilters && includeFilter && excludeFilter;
240 };
241 let vmselector = me.down('#vms');
242 vmselector.getStore().setFilters({
243 id: 'customFilter',
244 filterFn,
245 });
246 vmselector.checkChange();
247 if (me.action === 'migrateall') {
248 let records = vmselector.getSelection();
249 refreshLxcWarning(vmselector.getValue(), records);
250 }
251 };
252
253 items.push({
254 xtype: 'fieldset',
255 itemId: 'filters',
256 collapsible: true,
257 title: gettext('Filters'),
258 layout: 'hbox',
259 items: [
260 {
261 xtype: 'container',
262 flex: 1,
263 padding: 5,
264 layout: {
265 type: 'vbox',
266 align: 'stretch',
267 },
268 defaults: {
269 listeners: {
270 change: filterChange,
271 },
272 isFormField: false,
273 },
274 items: [
275 {
276 fieldLabel: gettext("Name"),
277 itemId: 'namefilter',
278 xtype: 'textfield',
279 },
280 {
281 xtype: 'combobox',
282 itemId: 'statusfilter',
283 fieldLabel: gettext("Status"),
284 emptyText: gettext('All'),
285 editable: false,
286 value: defaultStatus,
287 store: statusList,
288 },
289 {
290 xtype: 'combobox',
291 itemId: 'poolfilter',
292 fieldLabel: gettext("Pool"),
293 emptyText: gettext('All'),
294 editable: false,
295 multiSelect: true,
296 store: poolList,
297 },
298 ],
299 },
300 {
301 xtype: 'container',
302 layout: {
303 type: 'vbox',
304 align: 'stretch',
305 },
306 flex: 1,
307 padding: 5,
308 defaults: {
309 listeners: {
310 change: filterChange,
311 },
312 isFormField: false,
313 },
314 items: [
315 {
316 xtype: 'combobox',
317 itemId: 'typefilter',
318 fieldLabel: gettext("Type"),
319 emptyText: gettext('All'),
320 editable: false,
321 value: '',
322 store: [
323 ['', gettext('All')],
324 ['lxc', gettext('CT')],
325 ['qemu', gettext('VM')],
326 ],
327 },
328 {
329 xtype: 'proxmoxComboGrid',
330 itemId: 'includetagfilter',
331 fieldLabel: gettext("Include Tags"),
332 emptyText: gettext('All'),
333 editable: false,
334 multiSelect: true,
335 valueField: 'value',
336 displayField: 'value',
337 listConfig: {
338 userCls: 'proxmox-tags-full',
339 columns: [
340 {
341 dataIndex: 'value',
342 flex: 1,
343 renderer: value =>
344 PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
345 },
346 ],
347 },
348 store: {
349 data: tagList,
350 },
351 listeners: {
352 change: filterChange,
353 },
354 },
355 {
356 xtype: 'proxmoxComboGrid',
357 itemId: 'excludetagfilter',
358 fieldLabel: gettext("Exclude Tags"),
359 emptyText: gettext('None'),
360 multiSelect: true,
361 editable: false,
362 valueField: 'value',
363 displayField: 'value',
364 listConfig: {
365 userCls: 'proxmox-tags-full',
366 columns: [
367 {
368 dataIndex: 'value',
369 flex: 1,
370 renderer: value =>
371 PVE.Utils.renderTags(value, PVE.UIOptions.tagOverrides),
372 },
373 ],
374 },
375 store: {
376 data: tagList,
377 },
378 listeners: {
379 change: filterChange,
380 },
381 },
382 ],
383 },
384 {
385 xtype: 'container',
386 layout: {
387 type: 'vbox',
388 align: 'stretch',
389 },
390 flex: 1,
391 padding: 5,
392 defaults: {
393 listeners: {
394 change: filterChange,
395 },
396 isFormField: false,
397 },
398 items: [
399 {
400 xtype: 'combobox',
401 itemId: 'hastatefilter',
402 fieldLabel: gettext("HA status"),
403 emptyText: gettext('All'),
404 multiSelect: true,
405 editable: false,
406 store: haList,
407 listeners: {
408 change: filterChange,
409 },
410 },
411 {
412 xtype: 'container',
413 layout: {
414 type: 'vbox',
415 align: 'end',
416 },
417 items: [
418 {
419 xtype: 'button',
420 itemId: 'clearBtn',
421 text: gettext('Clear Filters'),
422 disabled: true,
423 handler: clearFilters,
424 },
425 ],
426 },
427 ],
428 },
429 ],
430 });
431
432 items.push({
433 xtype: 'vmselector',
434 itemId: 'vms',
435 name: 'vms',
436 flex: 1,
437 height: 300,
438 selectAll: true,
439 allowBlank: false,
440 plugins: '',
441 nodename: me.nodename,
442 listeners: {
443 selectionchange: function(vmselector, records) {
444 if (me.action === 'migrateall') {
445 let vmids = me.down('#vms').getValue();
446 refreshLxcWarning(vmids, records);
447 }
448 },
449 },
450 });
451
452 me.formPanel = Ext.create('Ext.form.Panel', {
453 bodyPadding: 10,
454 border: false,
455 layout: {
456 type: 'vbox',
457 align: 'stretch',
458 },
459 fieldDefaults: {
460 anchor: '100%',
461 },
462 items: items,
463 });
464
465 let form = me.formPanel.getForm();
466
467 let submitBtn = Ext.create('Ext.Button', {
468 text: me.btnText,
469 handler: function() {
470 form.isValid();
471 me.submit(form.getValues());
472 },
473 });
474
475 Ext.apply(me, {
476 items: [me.formPanel],
477 buttons: [submitBtn],
478 });
479
480 me.callParent();
481
482 form.on('validitychange', function() {
483 let valid = form.isValid();
484 submitBtn.setDisabled(!valid);
485 });
486 form.isValid();
487
488 filterChange();
489 },
490 });