]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/window/BulkAction.js
ui: guest import: network grid: allow selecting hardware type
[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`, `suspendall`
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 let defaultType = me.action === 'suspendall' ? 'qemu' : '';
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
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
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');
221 let clearBtn = me.down('#clearBtn');
222 if (filterCount) {
223 fieldSet.setTitle(Ext.String.format(gettext('Filters ({0})'), filterCount));
224 clearBtn.setDisabled(false);
225 } else {
226 fieldSet.setTitle(gettext('Filters'));
227 clearBtn.setDisabled(true);
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,
322 value: defaultType,
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 },
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 },
428 ],
429 },
430 ],
431 });
432
433 items.push({
434 xtype: 'vmselector',
435 itemId: 'vms',
436 name: 'vms',
437 flex: 1,
438 height: 300,
439 selectAll: true,
440 allowBlank: false,
441 plugins: '',
442 nodename: me.nodename,
443 listeners: {
444 selectionchange: function(vmselector, records) {
445 if (me.action === 'migrateall') {
446 let vmids = me.down('#vms').getValue();
447 refreshLxcWarning(vmids, records);
448 }
449 },
450 },
451 });
452
453 me.formPanel = Ext.create('Ext.form.Panel', {
454 bodyPadding: 10,
455 border: false,
456 layout: {
457 type: 'vbox',
458 align: 'stretch',
459 },
460 fieldDefaults: {
461 anchor: '100%',
462 },
463 items: items,
464 });
465
466 let form = me.formPanel.getForm();
467
468 let submitBtn = Ext.create('Ext.Button', {
469 text: me.btnText,
470 handler: function() {
471 form.isValid();
472 me.submit(form.getValues());
473 },
474 });
475
476 Ext.apply(me, {
477 items: [me.formPanel],
478 buttons: [submitBtn],
479 });
480
481 me.callParent();
482
483 form.on('validitychange', function() {
484 let valid = form.isValid();
485 submitBtn.setDisabled(!valid);
486 });
487 form.isValid();
488
489 filterChange();
490 },
491 });