]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/qemu/HardwareView.js
gui: qemu/HardwareView: prepare Hardwareview for font-awesome icons
[pve-manager.git] / www / manager6 / qemu / HardwareView.js
1 Ext.define('PVE.qemu.HardwareView', {
2 extend: 'Proxmox.grid.PendingObjectGrid',
3 alias: ['widget.PVE.qemu.HardwareView'],
4
5 onlineHelp: 'qm_virtual_machines_settings',
6
7 renderKey: function(key, metaData, rec, rowIndex, colIndex, store) {
8 var me = this;
9 var rows = me.rows;
10 var rowdef = rows[key] || {};
11 var iconCls = rowdef.iconCls;
12 var icon = '';
13 var txt = (rowdef.header || key);
14
15 metaData.tdAttr = "valign=middle";
16
17 if (rowdef.tdCls) {
18 metaData.tdCls = rowdef.tdCls;
19 if (rowdef.tdCls == 'pve-itype-icon-storage') {
20 var value = me.getObjectValue(key, '', false);
21 if (value === '') {
22 value = me.getObjectValue(key, '', true);
23 }
24 if (value.match(/vm-.*-cloudinit/)) {
25 metaData.tdCls = 'pve-itype-icon-cloud';
26 return rowdef.cloudheader;
27 } else if (value.match(/media=cdrom/)) {
28 metaData.tdCls = 'pve-itype-icon-cdrom';
29 return rowdef.cdheader;
30 }
31 }
32 } else if (iconCls) {
33 icon = "<i class='pve-grid-fa fa fa-fw fa-" + iconCls + "'></i>";
34 metaData.tdCls += " pve-itype-fa";
35 }
36 return icon + txt;
37 },
38
39 initComponent : function() {
40 var me = this;
41 var i, confid;
42
43 var nodename = me.pveSelNode.data.node;
44 if (!nodename) {
45 throw "no node name specified";
46 }
47
48 var vmid = me.pveSelNode.data.vmid;
49 if (!vmid) {
50 throw "no VM ID specified";
51 }
52
53 var caps = Ext.state.Manager.get('GuiCap');
54
55 var rows = {
56 memory: {
57 header: gettext('Memory'),
58 editor: caps.vms['VM.Config.Memory'] ? 'PVE.qemu.MemoryEdit' : undefined,
59 never_delete: true,
60 defaultValue: '512',
61 tdCls: 'pve-itype-icon-memory',
62 group: 2,
63 multiKey: ['memory', 'balloon', 'shares'],
64 renderer: function(value, metaData, record, ri, ci, store, pending) {
65 var res = '';
66
67 var max = me.getObjectValue('memory', 512, pending);
68 var balloon = me.getObjectValue('balloon', undefined, pending);
69 var shares = me.getObjectValue('shares', undefined, pending);
70
71 res = Proxmox.Utils.format_size(max*1024*1024);
72
73 if (balloon !== undefined && balloon > 0) {
74 res = Proxmox.Utils.format_size(balloon*1024*1024) + "/" + res;
75
76 if (shares) {
77 res += ' [shares=' + shares +']';
78 }
79 } else if (balloon === 0) {
80 res += ' [balloon=0]';
81 }
82 return res;
83 }
84 },
85 sockets: {
86 header: gettext('Processors'),
87 never_delete: true,
88 editor: (caps.vms['VM.Config.CPU'] || caps.vms['VM.Config.HWType']) ?
89 'PVE.qemu.ProcessorEdit' : undefined,
90 tdCls: 'pve-itype-icon-processor',
91 group: 3,
92 defaultValue: '1',
93 multiKey: ['sockets', 'cpu', 'cores', 'numa', 'vcpus', 'cpulimit', 'cpuunits'],
94 renderer: function(value, metaData, record, rowIndex, colIndex, store, pending) {
95
96 var sockets = me.getObjectValue('sockets', 1, pending);
97 var model = me.getObjectValue('cpu', undefined, pending);
98 var cores = me.getObjectValue('cores', 1, pending);
99 var numa = me.getObjectValue('numa', undefined, pending);
100 var vcpus = me.getObjectValue('vcpus', undefined, pending);
101 var cpulimit = me.getObjectValue('cpulimit', undefined, pending);
102 var cpuunits = me.getObjectValue('cpuunits', undefined, pending);
103
104 var res = Ext.String.format('{0} ({1} sockets, {2} cores)',
105 sockets*cores, sockets, cores);
106
107 if (model) {
108 res += ' [' + model + ']';
109 }
110
111 if (numa) {
112 res += ' [numa=' + numa +']';
113 }
114
115 if (vcpus) {
116 res += ' [vcpus=' + vcpus +']';
117 }
118
119 if (cpulimit) {
120 res += ' [cpulimit=' + cpulimit +']';
121 }
122
123 if (cpuunits) {
124 res += ' [cpuunits=' + cpuunits +']';
125 }
126
127 return res;
128 }
129 },
130 keyboard: {
131 header: gettext('Keyboard Layout'),
132 never_delete: true,
133 editor: caps.vms['VM.Config.Options'] ? 'PVE.qemu.KeyboardEdit' : undefined,
134 tdCls: 'pve-itype-icon-keyboard',
135 group: 1,
136 defaultValue: '',
137 renderer: PVE.Utils.render_kvm_language
138 },
139 vga: {
140 header: gettext('Display'),
141 editor: caps.vms['VM.Config.HWType'] ? 'PVE.qemu.DisplayEdit' : undefined,
142 never_delete: true,
143 tdCls: 'pve-itype-icon-display',
144 group:4,
145 defaultValue: '',
146 renderer: PVE.Utils.render_kvm_vga_driver
147 },
148 cores: {
149 visible: false
150 },
151 cpu: {
152 visible: false
153 },
154 numa: {
155 visible: false
156 },
157 balloon: {
158 visible: false
159 },
160 hotplug: {
161 visible: false
162 },
163 vcpus: {
164 visible: false
165 },
166 cpuunits: {
167 visible: false
168 },
169 cpulimit: {
170 visible: false
171 },
172 bios: {
173 visible: false
174 },
175 shares: {
176 visible: false
177 }
178 };
179
180 PVE.Utils.forEachBus(undefined, function(type, id) {
181 var confid = type + id;
182 rows[confid] = {
183 group: 5,
184 tdCls: 'pve-itype-icon-storage',
185 editor: 'PVE.qemu.HDEdit',
186 never_delete: caps.vms['VM.Config.Disk'] ? false : true,
187 header: gettext('Hard Disk') + ' (' + confid +')',
188 cdheader: gettext('CD/DVD Drive') + ' (' + confid +')',
189 cloudheader: gettext('CloudInit Drive') + ' (' + confid + ')'
190 };
191 });
192 for (i = 0; i < 32; i++) {
193 confid = "net" + i.toString();
194 rows[confid] = {
195 group: 6,
196 order: i,
197 tdCls: 'pve-itype-icon-network',
198 editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.NetworkEdit' : undefined,
199 never_delete: caps.vms['VM.Config.Network'] ? false : true,
200 header: gettext('Network Device') + ' (' + confid +')'
201 };
202 }
203 rows.efidisk0 = {
204 group: 7,
205 tdCls: 'pve-itype-icon-storage',
206 editor: null,
207 never_delete: caps.vms['VM.Config.Disk'] ? false : true,
208 header: gettext('EFI Disk')
209 };
210 for (i = 0; i < 5; i++) {
211 confid = "usb" + i.toString();
212 rows[confid] = {
213 group: 8,
214 order: i,
215 tdCls: 'pve-itype-icon-usb',
216 editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.USBEdit' : undefined,
217 never_delete: caps.nodes['Sys.Console'] ? false : true,
218 header: gettext('USB Device') + ' (' + confid + ')'
219 };
220 }
221 for (i = 0; i < 4; i++) {
222 confid = "hostpci" + i.toString();
223 rows[confid] = {
224 group: 9,
225 order: i,
226 tdCls: 'pve-itype-icon-pci',
227 never_delete: caps.nodes['Sys.Console'] ? false : true,
228 editor: caps.nodes['Sys.Console'] ? 'PVE.qemu.PCIEdit' : undefined,
229 header: gettext('PCI Device') + ' (' + confid + ')'
230 };
231 }
232 for (i = 0; i < 4; i++) {
233 confid = "serial" + i.toString();
234 rows[confid] = {
235 group: 10,
236 order: i,
237 tdCls: 'pve-itype-icon-serial',
238 never_delete: caps.nodes['Sys.Console'] ? false : true,
239 header: gettext('Serial Port') + ' (' + confid + ')'
240 };
241 }
242 for (i = 0; i < 256; i++) {
243 rows["unused" + i.toString()] = {
244 group: 99,
245 order: i,
246 tdCls: 'pve-itype-icon-storage',
247 editor: caps.vms['VM.Config.Disk'] ? 'PVE.qemu.HDEdit' : undefined,
248 header: gettext('Unused Disk') + ' ' + i.toString()
249 };
250 }
251
252 var sorterFn = function(rec1, rec2) {
253 var v1 = rec1.data.key;
254 var v2 = rec2.data.key;
255 var g1 = rows[v1].group || 0;
256 var g2 = rows[v2].group || 0;
257 var order1 = rows[v1].order || 0;
258 var order2 = rows[v2].order || 0;
259
260 if ((g1 - g2) !== 0) {
261 return g1 - g2;
262 }
263
264 if ((order1 - order2) !== 0) {
265 return order1 - order2;
266 }
267
268 if (v1 > v2) {
269 return 1;
270 } else if (v1 < v2) {
271 return -1;
272 } else {
273 return 0;
274 }
275 };
276
277 var reload = function() {
278 me.rstore.load();
279 };
280
281 var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config';
282
283 var sm = Ext.create('Ext.selection.RowModel', {});
284
285 var run_editor = function() {
286 var rec = sm.getSelection()[0];
287 if (!rec) {
288 return;
289 }
290
291 var rowdef = rows[rec.data.key];
292 if (!rowdef.editor) {
293 return;
294 }
295
296 var editor = rowdef.editor;
297 if (rowdef.tdCls == 'pve-itype-icon-storage') {
298 var value = me.getObjectValue(rec.data.key, '', true);
299 if (value.match(/vm-.*-cloudinit/)) {
300 return;
301 } else if (value.match(/media=cdrom/)) {
302 editor = 'PVE.qemu.CDEdit';
303 }
304 }
305
306 var win;
307
308 if (Ext.isString(editor)) {
309 win = Ext.create(editor, {
310 pveSelNode: me.pveSelNode,
311 confid: rec.data.key,
312 url: '/api2/extjs/' + baseurl
313 });
314 } else {
315 var config = Ext.apply({
316 pveSelNode: me.pveSelNode,
317 confid: rec.data.key,
318 url: '/api2/extjs/' + baseurl
319 }, rowdef.editor);
320 win = Ext.createWidget(rowdef.editor.xtype, config);
321 win.load();
322 }
323
324 win.show();
325 win.on('destroy', reload);
326 };
327
328 var run_resize = function() {
329 var rec = sm.getSelection()[0];
330 if (!rec) {
331 return;
332 }
333
334 var win = Ext.create('PVE.window.HDResize', {
335 disk: rec.data.key,
336 nodename: nodename,
337 vmid: vmid
338 });
339
340 win.show();
341
342 win.on('destroy', reload);
343 };
344
345 var run_move = function() {
346 var rec = sm.getSelection()[0];
347 if (!rec) {
348 return;
349 }
350
351 var win = Ext.create('PVE.window.HDMove', {
352 disk: rec.data.key,
353 nodename: nodename,
354 vmid: vmid
355 });
356
357 win.show();
358
359 win.on('destroy', reload);
360 };
361
362 var edit_btn = new Proxmox.button.Button({
363 text: gettext('Edit'),
364 selModel: sm,
365 disabled: true,
366 handler: run_editor
367 });
368
369 var resize_btn = new Proxmox.button.Button({
370 text: gettext('Resize disk'),
371 selModel: sm,
372 disabled: true,
373 handler: run_resize
374 });
375
376 var move_btn = new Proxmox.button.Button({
377 text: gettext('Move disk'),
378 selModel: sm,
379 disabled: true,
380 handler: run_move
381 });
382
383 var remove_btn = new Proxmox.button.Button({
384 text: gettext('Remove'),
385 defaultText: gettext('Remove'),
386 altText: gettext('Detach'),
387 selModel: sm,
388 disabled: true,
389 dangerous: true,
390 RESTMethod: 'PUT',
391 confirmMsg: function(rec) {
392 var warn = gettext('Are you sure you want to remove entry {0}');
393 if (this.text === this.altText) {
394 warn = gettext('Are you sure you want to detach entry {0}');
395 }
396
397 var entry = rec.data.key;
398 var msg = Ext.String.format(warn, "'"
399 + me.renderKey(entry, {}, rec) + "'");
400
401 if (entry.match(/^unused\d+$/)) {
402 msg += " " + gettext('This will permanently erase all data.');
403 }
404
405 return msg;
406 },
407 handler: function(b, e, rec) {
408 Proxmox.Utils.API2Request({
409 url: '/api2/extjs/' + baseurl,
410 waitMsgTarget: me,
411 method: b.RESTMethod,
412 params: {
413 'delete': rec.data.key
414 },
415 callback: function() {
416 reload();
417 },
418 failure: function (response, opts) {
419 Ext.Msg.alert('Error', response.htmlStatus);
420 },
421 success: function(response, options) {
422 if (b.RESTMethod === 'POST') {
423 var upid = response.result.data;
424 var win = Ext.create('Proxmox.window.TaskProgress', {
425 upid: upid,
426 listeners: {
427 destroy: function () {
428 me.reload();
429 }
430 }
431 });
432 win.show();
433 }
434 }
435 });
436 },
437 listeners: {
438 render: function(btn) {
439 // hack: calculate an optimal button width on first display
440 // to prevent the whole toolbar to move when we switch
441 // between the "Remove" and "Detach" labels
442 var def = btn.getSize().width;
443
444 btn.setText(btn.altText);
445 var alt = btn.getSize().width;
446
447 btn.setText(btn.defaultText);
448
449 var optimal = alt > def ? alt : def;
450 btn.setSize({ width: optimal });
451 }
452 }
453 });
454
455 var revert_btn = new Proxmox.button.Button({
456 text: gettext('Revert'),
457 selModel: sm,
458 disabled: true,
459 handler: function(b, e, rec) {
460 var rowdef = me.rows[rec.data.key] || {};
461 var keys = rowdef.multiKey || [ rec.data.key ];
462 var revert = keys.join(',');
463 Proxmox.Utils.API2Request({
464 url: '/api2/extjs/' + baseurl,
465 waitMsgTarget: me,
466 method: 'PUT',
467 params: {
468 'revert': revert
469 },
470 callback: function() {
471 reload();
472 },
473 failure: function (response, opts) {
474 Ext.Msg.alert('Error',response.htmlStatus);
475 }
476 });
477 }
478 });
479
480 var efidisk_menuitem = Ext.create('Ext.menu.Item',{
481 text: gettext('EFI Disk'),
482 iconCls: 'pve-itype-icon-storage',
483 disabled: !caps.vms['VM.Config.Disk'],
484 handler: function() {
485
486 var rstoredata = me.rstore.getData().map;
487 // check if ovmf is configured
488 if (rstoredata.bios && rstoredata.bios.data.value === 'ovmf') {
489 var win = Ext.create('PVE.qemu.EFIDiskEdit', {
490 url: '/api2/extjs/' + baseurl,
491 pveSelNode: me.pveSelNode
492 });
493 win.on('destroy', reload);
494 win.show();
495 } else {
496 Ext.Msg.alert('Error',gettext('Please select OVMF(UEFI) as BIOS first.'));
497 }
498
499 }
500 });
501
502 var set_button_status = function() {
503 var sm = me.getSelectionModel();
504 var rec = sm.getSelection()[0];
505
506 // disable button when we have an efidisk already
507 // disable is ok in this case, because you can instantly
508 // see that there is already one
509 efidisk_menuitem.setDisabled(me.rstore.getData().map.efidisk0 !== undefined);
510 // en/disable usb add button
511 var usbcount = 0;
512 var pcicount = 0;
513 var hasCloudInit = false;
514 me.rstore.getData().items.forEach(function(item){
515 if (/^usb\d+/.test(item.id)) {
516 usbcount++;
517 } else if (/^hostpci\d+/.test(item.id)) {
518 pcicount++;
519 }
520 if (!hasCloudInit && /vm-.*-cloudinit/.test(item.data.value)) {
521 hasCloudInit = true;
522 }
523 });
524
525 // heuristic only for disabling some stuff, the backend has the final word.
526 var noSysConsolePerm = !caps.nodes['Sys.Console'];
527
528 me.down('#addusb').setDisabled(noSysConsolePerm || (usbcount >= 5));
529 me.down('#addpci').setDisabled(noSysConsolePerm || (pcicount >= 4));
530 me.down('#addci').setDisabled(noSysConsolePerm || hasCloudInit);
531
532 if (!rec) {
533 remove_btn.disable();
534 edit_btn.disable();
535 resize_btn.disable();
536 move_btn.disable();
537 revert_btn.disable();
538 return;
539 }
540 var key = rec.data.key;
541 var value = rec.data.value;
542 var rowdef = rows[key];
543
544 var pending = rec.data['delete'] || me.hasPendingChanges(key);
545 var isUnusedDisk = key.match(/^unused\d+/);
546 var isUsedDisk = !isUnusedDisk &&
547 rowdef.tdCls == 'pve-itype-icon-storage' &&
548 (value && !value.match(/media=cdrom/));
549
550 var isCloudInit = (value && value.toString().match(/vm-.*-cloudinit/));
551
552 var isEfi = (key === 'efidisk0');
553
554 remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true));
555 remove_btn.setText((isUsedDisk && !isCloudInit) ? remove_btn.altText : remove_btn.defaultText);
556 remove_btn.RESTMethod = isUnusedDisk ? 'POST':'PUT';
557
558 edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor || isCloudInit);
559
560 resize_btn.setDisabled(pending || !isUsedDisk);
561
562 move_btn.setDisabled(pending || !isUsedDisk);
563
564 revert_btn.setDisabled(!pending);
565
566 };
567
568 Ext.apply(me, {
569 url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending',
570 interval: 5000,
571 selModel: sm,
572 run_editor: run_editor,
573 tbar: [
574 {
575 text: gettext('Add'),
576 menu: new Ext.menu.Menu({
577 items: [
578 {
579 text: gettext('Hard Disk'),
580 iconCls: 'pve-itype-icon-storage',
581 disabled: !caps.vms['VM.Config.Disk'],
582 handler: function() {
583 var win = Ext.create('PVE.qemu.HDEdit', {
584 url: '/api2/extjs/' + baseurl,
585 pveSelNode: me.pveSelNode
586 });
587 win.on('destroy', reload);
588 win.show();
589 }
590 },
591 {
592 text: gettext('CD/DVD Drive'),
593 iconCls: 'pve-itype-icon-cdrom',
594 disabled: !caps.vms['VM.Config.Disk'],
595 handler: function() {
596 var win = Ext.create('PVE.qemu.CDEdit', {
597 url: '/api2/extjs/' + baseurl,
598 pveSelNode: me.pveSelNode
599 });
600 win.on('destroy', reload);
601 win.show();
602 }
603 },
604 {
605 text: gettext('Network Device'),
606 iconCls: 'pve-itype-icon-network',
607 disabled: !caps.vms['VM.Config.Network'],
608 handler: function() {
609 var win = Ext.create('PVE.qemu.NetworkEdit', {
610 url: '/api2/extjs/' + baseurl,
611 pveSelNode: me.pveSelNode
612 });
613 win.on('destroy', reload);
614 win.show();
615 }
616 },
617 efidisk_menuitem,
618 {
619 text: gettext('USB Device'),
620 itemId: 'addusb',
621 iconCls: 'pve-itype-icon-usb',
622 disabled: !caps.nodes['Sys.Console'],
623 handler: function() {
624 var win = Ext.create('PVE.qemu.USBEdit', {
625 url: '/api2/extjs/' + baseurl,
626 pveSelNode: me.pveSelNode
627 });
628 win.on('destroy', reload);
629 win.show();
630 }
631 },
632 {
633 text: gettext('PCI Device'),
634 itemId: 'addpci',
635 iconCls: 'pve-itype-icon-pci',
636 disabled: !caps.nodes['Sys.Console'],
637 handler: function() {
638 var win = Ext.create('PVE.qemu.PCIEdit', {
639 url: '/api2/extjs/' + baseurl,
640 pveSelNode: me.pveSelNode
641 });
642 win.on('destroy', reload);
643 win.show();
644 }
645 },
646 {
647 text: gettext('Serial Port'),
648 itemId: 'addserial',
649 iconCls: 'pve-itype-icon-serial',
650 disabled: !caps.vms['VM.Config.Options'],
651 handler: function() {
652 var win = Ext.create('PVE.qemu.SerialEdit', {
653 url: '/api2/extjs/' + baseurl
654 });
655 win.on('destroy', reload);
656 win.show();
657 }
658 },
659 {
660 text: gettext('CloudInit Drive'),
661 itemId: 'addci',
662 iconCls: 'pve-itype-icon-cloud',
663 disabled: !caps.nodes['Sys.Console'],
664 handler: function() {
665 var win = Ext.create('PVE.qemu.CIDriveEdit', {
666 url: '/api2/extjs/' + baseurl,
667 pveSelNode: me.pveSelNode
668 });
669 win.on('destroy', reload);
670 win.show();
671 }
672 }
673 ]
674 })
675 },
676 remove_btn,
677 edit_btn,
678 resize_btn,
679 move_btn,
680 revert_btn
681 ],
682 rows: rows,
683 sorterFn: sorterFn,
684 listeners: {
685 itemdblclick: run_editor,
686 selectionchange: set_button_status
687 }
688 });
689
690 me.callParent();
691
692 me.on('activate', me.rstore.startUpdate);
693 me.on('destroy', me.rstore.stopUpdate);
694
695 me.mon(me.rstore, 'refresh', function() {
696 set_button_status();
697 });
698 }
699 });