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