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