]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/Backup.js
ui: backup job detail view: simplify enabled renderer
[pve-manager.git] / www / manager6 / dc / Backup.js
1 Ext.define('PVE.dc.BackupEdit', {
2 extend: 'Proxmox.window.Edit',
3 alias: ['widget.pveDcBackupEdit'],
4
5 defaultFocus: undefined,
6
7 initComponent: function() {
8 let me = this;
9
10 me.isCreate = !me.jobid;
11
12 let url, method;
13 if (me.isCreate) {
14 url = '/api2/extjs/cluster/backup';
15 method = 'POST';
16 } else {
17 url = '/api2/extjs/cluster/backup/' + me.jobid;
18 method = 'PUT';
19 }
20
21 let vmidField = Ext.create('Ext.form.field.Hidden', {
22 name: 'vmid',
23 });
24
25 // 'value' can be assigned a string or an array
26 let selModeField = Ext.create('Proxmox.form.KVComboBox', {
27 xtype: 'proxmoxKVComboBox',
28 comboItems: [
29 ['include', gettext('Include selected VMs')],
30 ['all', gettext('All')],
31 ['exclude', gettext('Exclude selected VMs')],
32 ['pool', gettext('Pool based')],
33 ],
34 fieldLabel: gettext('Selection mode'),
35 name: 'selMode',
36 value: '',
37 });
38
39 let sm = Ext.create('Ext.selection.CheckboxModel', {
40 mode: 'SIMPLE',
41 listeners: {
42 selectionchange: function(model, selected) {
43 var sel = [];
44 Ext.Array.each(selected, function(record) {
45 sel.push(record.data.vmid);
46 });
47
48 // to avoid endless recursion suspend the vmidField change
49 // event temporary as it calls us again
50 vmidField.suspendEvent('change');
51 vmidField.setValue(sel);
52 vmidField.resumeEvent('change');
53 },
54 },
55 });
56
57 let storagesel = Ext.create('PVE.form.StorageSelector', {
58 fieldLabel: gettext('Storage'),
59 nodename: 'localhost',
60 storageContent: 'backup',
61 allowBlank: false,
62 name: 'storage',
63 listeners: {
64 change: function(f, v) {
65 let store = f.getStore();
66 let rec = store.findRecord('storage', v, 0, false, true, true);
67 let compressionSelector = me.down('pveCompressionSelector');
68
69 if (rec && rec.data && rec.data.type === 'pbs') {
70 compressionSelector.setValue('zstd');
71 compressionSelector.setDisabled(true);
72 } else if (!compressionSelector.getEditable()) {
73 compressionSelector.setDisabled(false);
74 }
75 },
76 },
77 });
78
79 let store = new Ext.data.Store({
80 model: 'PVEResources',
81 sorters: {
82 property: 'vmid',
83 order: 'ASC',
84 },
85 });
86
87 let vmgrid = Ext.createWidget('grid', {
88 store: store,
89 border: true,
90 height: 300,
91 selModel: sm,
92 disabled: true,
93 columns: [
94 {
95 header: 'ID',
96 dataIndex: 'vmid',
97 width: 60,
98 },
99 {
100 header: gettext('Node'),
101 dataIndex: 'node',
102 },
103 {
104 header: gettext('Status'),
105 dataIndex: 'uptime',
106 renderer: function(value) {
107 if (value) {
108 return Proxmox.Utils.runningText;
109 } else {
110 return Proxmox.Utils.stoppedText;
111 }
112 },
113 },
114 {
115 header: gettext('Name'),
116 dataIndex: 'name',
117 flex: 1,
118 },
119 {
120 header: gettext('Type'),
121 dataIndex: 'type',
122 },
123 ],
124 });
125
126 let selectPoolMembers = function(poolid) {
127 if (!poolid) {
128 return;
129 }
130 sm.deselectAll(true);
131 store.filter([
132 {
133 id: 'poolFilter',
134 property: 'pool',
135 value: poolid,
136 },
137 ]);
138 sm.selectAll(true);
139 };
140
141 let selPool = Ext.create('PVE.form.PoolSelector', {
142 fieldLabel: gettext('Pool to backup'),
143 hidden: true,
144 allowBlank: true,
145 name: 'pool',
146 listeners: {
147 change: function(selpool, newValue, oldValue) {
148 selectPoolMembers(newValue);
149 },
150 },
151 });
152
153 let nodesel = Ext.create('PVE.form.NodeSelector', {
154 name: 'node',
155 fieldLabel: gettext('Node'),
156 allowBlank: true,
157 editable: true,
158 autoSelect: false,
159 emptyText: '-- ' + gettext('All') + ' --',
160 listeners: {
161 change: function(f, value) {
162 storagesel.setNodename(value || 'localhost');
163 let mode = selModeField.getValue();
164 store.clearFilter();
165 store.filterBy(function(rec) {
166 return !value || rec.get('node') === value;
167 });
168 if (mode === 'all') {
169 sm.selectAll(true);
170 }
171 if (mode === 'pool') {
172 selectPoolMembers(selPool.value);
173 }
174 },
175 },
176 });
177
178 let column1 = [
179 nodesel,
180 storagesel,
181 {
182 xtype: 'pveDayOfWeekSelector',
183 name: 'dow',
184 fieldLabel: gettext('Day of week'),
185 multiSelect: true,
186 value: ['sat'],
187 allowBlank: false,
188 },
189 {
190 xtype: 'timefield',
191 fieldLabel: gettext('Start Time'),
192 name: 'starttime',
193 format: 'H:i',
194 formatText: 'HH:MM',
195 value: '00:00',
196 allowBlank: false,
197 },
198 selModeField,
199 selPool,
200 ];
201
202 let column2 = [
203 {
204 xtype: 'textfield',
205 fieldLabel: gettext('Send email to'),
206 name: 'mailto',
207 },
208 {
209 xtype: 'pveEmailNotificationSelector',
210 fieldLabel: gettext('Email notification'),
211 name: 'mailnotification',
212 deleteEmpty: !me.isCreate,
213 value: me.isCreate ? 'always' : '',
214 },
215 {
216 xtype: 'pveCompressionSelector',
217 fieldLabel: gettext('Compression'),
218 name: 'compress',
219 deleteEmpty: !me.isCreate,
220 value: 'zstd',
221 },
222 {
223 xtype: 'pveBackupModeSelector',
224 fieldLabel: gettext('Mode'),
225 value: 'snapshot',
226 name: 'mode',
227 },
228 {
229 xtype: 'proxmoxcheckbox',
230 fieldLabel: gettext('Enable'),
231 name: 'enabled',
232 uncheckedValue: 0,
233 defaultValue: 1,
234 checked: true,
235 },
236 vmidField,
237 ];
238
239 let ipanel = Ext.create('Proxmox.panel.InputPanel', {
240 onlineHelp: 'chapter_vzdump',
241 column1: column1,
242 column2: column2,
243 onGetValues: function(values) {
244 if (!values.node) {
245 if (!me.isCreate) {
246 Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
247 }
248 delete values.node;
249 }
250
251 let selMode = values.selMode;
252 delete values.selMode;
253
254 if (selMode === 'all') {
255 values.all = 1;
256 values.exclude = '';
257 delete values.vmid;
258 } else if (selMode === 'exclude') {
259 values.all = 1;
260 values.exclude = values.vmid;
261 delete values.vmid;
262 } else if (selMode === 'pool') {
263 delete values.vmid;
264 }
265
266 if (selMode !== 'pool') {
267 delete values.pool;
268 }
269 return values;
270 },
271 });
272
273 let update_vmid_selection = function(list, mode) {
274 if (mode !== 'all' && mode !== 'pool') {
275 sm.deselectAll(true);
276 if (list) {
277 Ext.Array.each(list.split(','), function(vmid) {
278 var rec = store.findRecord('vmid', vmid, 0, false, true, true);
279 if (rec) {
280 sm.select(rec, true);
281 }
282 });
283 }
284 }
285 };
286
287 vmidField.on('change', function(f, value) {
288 let mode = selModeField.getValue();
289 update_vmid_selection(value, mode);
290 });
291
292 selModeField.on('change', function(f, value, oldValue) {
293 if (oldValue === 'pool') {
294 store.removeFilter('poolFilter');
295 }
296
297 if (oldValue === 'all') {
298 sm.deselectAll(true);
299 vmidField.setValue('');
300 }
301
302 if (value === 'all') {
303 sm.selectAll(true);
304 vmgrid.setDisabled(true);
305 } else {
306 vmgrid.setDisabled(false);
307 }
308
309 if (value === 'pool') {
310 vmgrid.setDisabled(true);
311 vmidField.setValue('');
312 selPool.setVisible(true);
313 selPool.allowBlank = false;
314 selectPoolMembers(selPool.value);
315 } else {
316 selPool.setVisible(false);
317 selPool.allowBlank = true;
318 }
319 let list = vmidField.getValue();
320 update_vmid_selection(list, value);
321 });
322
323 let reload = function() {
324 store.load({
325 params: {
326 type: 'vm',
327 },
328 callback: function() {
329 let node = nodesel.getValue();
330 store.clearFilter();
331 store.filterBy(rec => !node || node.length === 0 || rec.get('node') === node);
332 let list = vmidField.getValue();
333 let mode = selModeField.getValue();
334 if (mode === 'all') {
335 sm.selectAll(true);
336 } else if (mode === 'pool') {
337 selectPoolMembers(selPool.value);
338 } else {
339 update_vmid_selection(list, mode);
340 }
341 },
342 });
343 };
344
345 Ext.applyIf(me, {
346 subject: gettext("Backup Job"),
347 url: url,
348 method: method,
349 bodyPadding: 0,
350 items: [
351 {
352 xtype: 'tabpanel',
353 region: 'center',
354 layout: 'fit',
355 bodyPadding: 10,
356 items: [
357 {
358 xtype: 'container',
359 title: gettext('General'),
360 region: 'center',
361 layout: {
362 type: 'vbox',
363 align: 'stretch',
364 },
365 items: [
366 ipanel,
367 vmgrid,
368 ],
369 },
370 {
371 xtype: 'pveBackupJobPrunePanel',
372 title: gettext('Retention'),
373 isCreate: me.isCreate,
374 keepAllDefaultForCreate: false,
375 showPBSHint: false,
376 fallbackHintHtml: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
377 },
378 ],
379 },
380 ],
381
382 });
383
384 me.callParent();
385
386 if (me.isCreate) {
387 selModeField.setValue('include');
388 } else {
389 me.load({
390 success: function(response, options) {
391 let data = response.result.data;
392
393 data.dow = data.dow.split(',');
394
395 if (data.all || data.exclude) {
396 if (data.exclude) {
397 data.vmid = data.exclude;
398 data.selMode = 'exclude';
399 } else {
400 data.vmid = '';
401 data.selMode = 'all';
402 }
403 } else if (data.pool) {
404 data.selMode = 'pool';
405 data.selPool = data.pool;
406 } else {
407 data.selMode = 'include';
408 }
409
410 if (data['prune-backups']) {
411 Object.assign(data, data['prune-backups']);
412 delete data['prune-backups'];
413 } else if (data.maxfiles !== undefined) {
414 if (data.maxfiles > 0) {
415 data['keep-last'] = data.maxfiles;
416 } else {
417 data['keep-all'] = 1;
418 }
419 delete data.maxfiles;
420 }
421
422 me.setValues(data);
423 },
424 });
425 }
426
427 reload();
428 },
429 });
430
431
432 Ext.define('PVE.dc.BackupDiskTree', {
433 extend: 'Ext.tree.Panel',
434 alias: 'widget.pveBackupDiskTree',
435
436 folderSort: true,
437 rootVisible: false,
438
439 store: {
440 sorters: 'id',
441 data: {},
442 },
443
444 tools: [
445 {
446 type: 'expand',
447 tooltip: gettext('Expand All'),
448 callback: panel => panel.expandAll(),
449 },
450 {
451 type: 'collapse',
452 tooltip: gettext('Collapse All'),
453 callback: panel => panel.collapseAll(),
454 },
455 ],
456
457 columns: [
458 {
459 xtype: 'treecolumn',
460 text: gettext('Guest Image'),
461 renderer: function(value, meta, record) {
462 if (record.data.type) {
463 // guest level
464 let ret = value;
465 if (record.data.name) {
466 ret += " (" + record.data.name + ")";
467 }
468 return ret;
469 } else {
470 // extJS needs unique IDs but we only want to show the volumes key from "vmid:key"
471 return value.split(':')[1] + " - " + record.data.name;
472 }
473 },
474 dataIndex: 'id',
475 flex: 6,
476 },
477 {
478 text: gettext('Type'),
479 dataIndex: 'type',
480 flex: 1,
481 },
482 {
483 text: gettext('Backup Job'),
484 renderer: PVE.Utils.render_backup_status,
485 dataIndex: 'included',
486 flex: 3,
487 },
488 ],
489
490 reload: function() {
491 let me = this;
492 let sm = me.getSelectionModel();
493
494 Proxmox.Utils.API2Request({
495 url: `/cluster/backup/${me.jobid}/included_volumes`,
496 waitMsgTarget: me,
497 method: 'GET',
498 failure: function(response, opts) {
499 Proxmox.Utils.setErrorMask(me, response.htmlStatus);
500 },
501 success: function(response, opts) {
502 sm.deselectAll();
503 me.setRootNode(response.result.data);
504 me.expandAll();
505 },
506 });
507 },
508
509 initComponent: function() {
510 var me = this;
511
512 if (!me.jobid) {
513 throw "no job id specified";
514 }
515
516 var sm = Ext.create('Ext.selection.TreeModel', {});
517
518 Ext.apply(me, {
519 selModel: sm,
520 fields: ['id', 'type',
521 {
522 type: 'string',
523 name: 'iconCls',
524 calculate: function(data) {
525 var txt = 'fa x-fa-tree fa-';
526 if (data.leaf && !data.type) {
527 return txt + 'hdd-o';
528 } else if (data.type === 'qemu') {
529 return txt + 'desktop';
530 } else if (data.type === 'lxc') {
531 return txt + 'cube';
532 } else {
533 return txt + 'question-circle';
534 }
535 },
536 },
537 ],
538 header: {
539 items: [{
540 xtype: 'textfield',
541 fieldLabel: gettext('Search'),
542 labelWidth: 50,
543 emptyText: 'Name, VMID, Type',
544 width: 200,
545 padding: '0 5 0 0',
546 enableKeyEvents: true,
547 listeners: {
548 buffer: 500,
549 keyup: function(field) {
550 let searchValue = field.getValue().toLowerCase();
551 me.store.clearFilter(true);
552 me.store.filterBy(function(record) {
553 let data = {};
554 if (record.data.depth === 0) {
555 return true;
556 } else if (record.data.depth === 1) {
557 data = record.data;
558 } else if (record.data.depth === 2) {
559 data = record.parentNode.data;
560 }
561
562 for (const property of ['name', 'id', 'type']) {
563 if (!data[property]) {
564 continue;
565 }
566 let v = data[property].toString();
567 if (v !== undefined) {
568 v = v.toLowerCase();
569 if (v.includes(searchValue)) {
570 return true;
571 }
572 }
573 }
574 return false;
575 });
576 },
577 },
578 }],
579 },
580 });
581
582 me.callParent();
583
584 me.reload();
585 },
586 });
587
588 Ext.define('PVE.dc.BackupInfo', {
589 extend: 'Proxmox.panel.InputPanel',
590 alias: 'widget.pveBackupInfo',
591
592 viewModel: {
593 data: {
594 retentionType: 'none',
595 },
596 formulas: {
597 hasRetention: (get) => get('retentionType') !== 'none',
598 retentionKeepAll: (get) => get('retentionType') === 'all',
599 },
600 },
601
602 padding: '5 0 5 10',
603
604 column1: [
605 {
606 xtype: 'displayfield',
607 name: 'node',
608 fieldLabel: gettext('Node'),
609 renderer: function(value) {
610 if (!value) {
611 return '-- ' + gettext('All') + ' --';
612 } else {
613 return value;
614 }
615 },
616 },
617 {
618 xtype: 'displayfield',
619 name: 'storage',
620 fieldLabel: gettext('Storage'),
621 },
622 {
623 xtype: 'displayfield',
624 name: 'dow',
625 fieldLabel: gettext('Day of week'),
626 renderer: PVE.Utils.render_backup_days_of_week,
627 },
628 {
629 xtype: 'displayfield',
630 name: 'starttime',
631 fieldLabel: gettext('Start Time'),
632 },
633 {
634 xtype: 'displayfield',
635 name: 'selMode',
636 fieldLabel: gettext('Selection mode'),
637 },
638 {
639 xtype: 'displayfield',
640 name: 'pool',
641 fieldLabel: gettext('Pool to backup'),
642 },
643 ],
644 column2: [
645 {
646 xtype: 'displayfield',
647 name: 'mailto',
648 fieldLabel: gettext('Send email to'),
649 },
650 {
651 xtype: 'displayfield',
652 name: 'mailnotification',
653 fieldLabel: gettext('Email notification'),
654 renderer: function(value) {
655 let msg;
656 switch (value) {
657 case 'always':
658 msg = gettext('Always');
659 break;
660 case 'failure':
661 msg = gettext('On failure only');
662 break;
663 }
664 return msg;
665 },
666 },
667 {
668 xtype: 'displayfield',
669 name: 'compress',
670 fieldLabel: gettext('Compression'),
671 },
672 {
673 xtype: 'displayfield',
674 name: 'mode',
675 fieldLabel: gettext('Mode'),
676 renderer: function(value) {
677 let msg;
678 switch (value) {
679 case 'snapshot':
680 msg = gettext('Snapshot');
681 break;
682 case 'suspend':
683 msg = gettext('Suspend');
684 break;
685 case 'stop':
686 msg = gettext('Stop');
687 break;
688 }
689 return msg;
690 },
691 },
692 {
693 xtype: 'displayfield',
694 name: 'enabled',
695 fieldLabel: gettext('Enabled'),
696 renderer: v => PVE.Parser.parseBoolean(v.toString()) ? gettext('Yes') : gettext('No'),
697 },
698 ],
699
700 columnB: [
701 {
702 xtype: 'label',
703 name: 'pruneLabel',
704 text: gettext('Retention Configuration') + ':',
705 bind: {
706 hidden: '{!hasRetention}',
707 },
708 },
709 {
710 layout: 'hbox',
711 border: false,
712 defaults: {
713 border: false,
714 layout: 'anchor',
715 flex: 1,
716 },
717 items: [
718 {
719 padding: '0 10 0 0',
720 defaults: {
721 labelWidth: 110,
722 },
723 items: [{
724 xtype: 'displayfield',
725 name: 'keep-all',
726 fieldLabel: gettext('Keep All'),
727 renderer: Proxmox.Utils.format_boolean,
728 bind: {
729 hidden: '{!retentionKeepAll}',
730 },
731 }].concat(
732 [
733 ['keep-last', gettext('Keep Last')],
734 ['keep-daily', gettext('Keep Daily')],
735 ['keep-monthly', gettext('Keep Monthly')],
736 ].map(
737 name => ({
738 xtype: 'displayfield',
739 name: name[0],
740 fieldLabel: name[1],
741 bind: {
742 hidden: '{!hasRetention || retentionKeepAll}',
743 },
744 }),
745 ),
746 ),
747 },
748 {
749 padding: '0 0 0 10',
750 defaults: {
751 labelWidth: 110,
752 },
753 items: [
754 ['keep-hourly', gettext('Keep Hourly')],
755 ['keep-weekly', gettext('Keep Weekly')],
756 ['keep-yearly', gettext('Keep Yearly')],
757 ].map(
758 name => ({
759 xtype: 'displayfield',
760 name: name[0],
761 fieldLabel: name[1],
762 bind: {
763 hidden: '{!hasRetention || retentionKeepAll}',
764 },
765 }),
766 ),
767 },
768 ],
769 },
770 ],
771
772 setValues: function(values) {
773 var me = this;
774 let vm = me.getViewModel();
775
776 Ext.iterate(values, function(fieldId, val) {
777 let field = me.query('[isFormField][name=' + fieldId + ']')[0];
778 if (field) {
779 field.setValue(val);
780 }
781 });
782
783 if (values['prune-backups'] || values.maxfiles !== undefined) {
784 const keepNames = [
785 'keep-all',
786 'keep-last',
787 'keep-hourly',
788 'keep-daily',
789 'keep-weekly',
790 'keep-monthly',
791 'keep-yearly',
792 ];
793
794 let keepValues;
795 if (values['prune-backups']) {
796 keepValues = values['prune-backups'];
797 } else if (values.maxfiles > 0) {
798 keepValues = { 'keep-last': values.maxfiles };
799 } else {
800 keepValues = { 'keep-all': 1 };
801 }
802
803 vm.set('retentionType', keepValues['keep-all'] ? 'all' : 'other');
804
805 keepNames.forEach(function(name) {
806 let field = me.query('[isFormField][name=' + name + ']')[0];
807 if (field) {
808 field.setValue(keepValues[name]);
809 }
810 });
811 } else {
812 vm.set('retentionType', 'none');
813 }
814
815 // selection Mode depends on the presence/absence of several keys
816 let selModeField = me.query('[isFormField][name=selMode]')[0];
817 let selMode = 'none';
818 if (values.vmid) {
819 selMode = gettext('Include selected VMs');
820 }
821 if (values.all) {
822 selMode = gettext('All');
823 }
824 if (values.exclude) {
825 selMode = gettext('Exclude selected VMs');
826 }
827 if (values.pool) {
828 selMode = gettext('Pool based');
829 }
830 selModeField.setValue(selMode);
831
832 if (!values.pool) {
833 let poolField = me.query('[isFormField][name=pool]')[0];
834 poolField.setVisible(0);
835 }
836 },
837
838 initComponent: function() {
839 var me = this;
840
841 if (!me.record) {
842 throw "no data provided";
843 }
844 me.callParent();
845
846 me.setValues(me.record);
847 },
848 });
849
850
851 Ext.define('PVE.dc.BackedGuests', {
852 extend: 'Ext.grid.GridPanel',
853 alias: 'widget.pveBackedGuests',
854
855 textfilter: '',
856
857 columns: [
858 {
859 header: gettext('Type'),
860 dataIndex: "type",
861 renderer: PVE.Utils.render_resource_type,
862 flex: 1,
863 sortable: true,
864 },
865 {
866 header: gettext('VMID'),
867 dataIndex: 'vmid',
868 flex: 1,
869 sortable: true,
870 },
871 {
872 header: gettext('Name'),
873 dataIndex: 'name',
874 flex: 2,
875 sortable: true,
876 },
877 ],
878
879 initComponent: function() {
880 let me = this;
881
882 me.store.clearFilter(true);
883
884 Ext.apply(me, {
885 stateful: true,
886 stateId: 'grid-dc-backed-guests',
887 tbar: [
888 '->',
889 gettext('Search') + ':', ' ',
890 {
891 xtype: 'textfield',
892 width: 200,
893 emptyText: 'Name, VMID, Type',
894 enableKeyEvents: true,
895 listeners: {
896 buffer: 500,
897 keyup: function(field) {
898 let searchValue = field.getValue().toLowerCase();
899 me.store.clearFilter(true);
900 me.store.filterBy(function(record) {
901 let data = record.data;
902 for (const property in ['name', 'id', 'type']) {
903 if (data[property] === null) {
904 continue;
905 }
906 let v = data[property].toString();
907 if (v !== undefined) {
908 v = v.toLowerCase();
909 if (v.includes(searchValue)) {
910 return true;
911 }
912 }
913 }
914 return false;
915 });
916 },
917 },
918 },
919 ],
920 viewConfig: {
921 stripeRows: true,
922 trackOver: false,
923 },
924 });
925 me.callParent();
926 },
927 });
928
929 Ext.define('PVE.dc.BackupView', {
930 extend: 'Ext.grid.GridPanel',
931
932 alias: ['widget.pveDcBackupView'],
933
934 onlineHelp: 'chapter_vzdump',
935
936 allText: '-- ' + gettext('All') + ' --',
937
938 initComponent: function() {
939 let me = this;
940
941 let store = new Ext.data.Store({
942 model: 'pve-cluster-backup',
943 proxy: {
944 type: 'proxmox',
945 url: "/api2/json/cluster/backup",
946 },
947 });
948
949 let not_backed_store = new Ext.data.Store({
950 sorters: 'vmid',
951 proxy: {
952 type: 'proxmox',
953 url: 'api2/json/cluster/backup-info/not-backed-up',
954 },
955 });
956
957 let noBackupJobWarning, noBackupJobInfoButton;
958 let reload = function() {
959 store.load();
960 not_backed_store.load({
961 callback: function(records, operation, success) {
962 noBackupJobWarning.setVisible(records.length > 0);
963 noBackupJobInfoButton.setVisible(records.length > 0);
964 },
965 });
966 };
967
968 let sm = Ext.create('Ext.selection.RowModel', {});
969
970 let run_editor = function() {
971 let rec = sm.getSelection()[0];
972 if (!rec) {
973 return;
974 }
975
976 let win = Ext.create('PVE.dc.BackupEdit', {
977 jobid: rec.data.id,
978 });
979 win.on('destroy', reload);
980 win.show();
981 };
982
983 let run_detail = function() {
984 let record = sm.getSelection()[0];
985 if (!record) {
986 return;
987 }
988 Ext.create('Ext.window.Window', {
989 modal: true,
990 width: 800,
991 height: 600,
992 stateful: true,
993 stateId: 'backup-detail-view',
994 resizable: true,
995 layout: 'fit',
996 title: gettext('Backup Details'),
997 items: [
998 {
999 xtype: 'panel',
1000 region: 'center',
1001 layout: {
1002 type: 'vbox',
1003 align: 'stretch',
1004 },
1005 items: [
1006 {
1007 xtype: 'pveBackupInfo',
1008 flex: 0,
1009 layout: 'fit',
1010 record: record.data,
1011 },
1012 {
1013 xtype: 'pveBackupDiskTree',
1014 title: gettext('Included disks'),
1015 flex: 1,
1016 jobid: record.data.id,
1017 },
1018 ],
1019 },
1020 ],
1021 }).show();
1022 };
1023
1024 let run_backup_now = function(job) {
1025 job = Ext.clone(job);
1026
1027 let jobNode = job.node;
1028 // Remove properties related to scheduling
1029 delete job.enabled;
1030 delete job.starttime;
1031 delete job.dow;
1032 delete job.id;
1033 delete job.node;
1034 job.all = job.all === true ? 1 : 0;
1035
1036 if (job['prune-backups']) {
1037 job['prune-backups'] = PVE.Parser.printPropertyString(job['prune-backups']);
1038 }
1039
1040 let allNodes = PVE.data.ResourceStore.getNodes();
1041 let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
1042 let errors = [];
1043
1044 if (jobNode !== undefined) {
1045 if (!nodes.includes(jobNode)) {
1046 Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
1047 return;
1048 }
1049 nodes = [jobNode];
1050 } else {
1051 let unkownNodes = allNodes.filter(node => node.status !== 'online');
1052 if (unkownNodes.length > 0) {errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));}
1053 }
1054 let jobTotalCount = nodes.length, jobsStarted = 0;
1055
1056 Ext.Msg.show({
1057 title: gettext('Please wait...'),
1058 closable: false,
1059 progress: true,
1060 progressText: '0/' + jobTotalCount,
1061 });
1062
1063 let postRequest = function() {
1064 jobsStarted++;
1065 Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
1066
1067 if (jobsStarted === jobTotalCount) {
1068 Ext.Msg.hide();
1069 if (errors.length > 0) {
1070 Ext.Msg.alert('Error', 'Some errors have been encountered:<br />' + errors.join('<br />'));
1071 }
1072 }
1073 };
1074
1075 nodes.forEach(node => Proxmox.Utils.API2Request({
1076 url: '/nodes/' + node + '/vzdump',
1077 method: 'POST',
1078 params: job,
1079 failure: function(response, opts) {
1080 errors.push(node + ': ' + response.htmlStatus);
1081 postRequest();
1082 },
1083 success: postRequest,
1084 }));
1085 };
1086
1087 let run_show_not_backed = function() {
1088 Ext.create('Ext.window.Window', {
1089 modal: true,
1090 width: 600,
1091 height: 500,
1092 resizable: true,
1093 layout: 'fit',
1094 title: gettext('Guests without backup job'),
1095 items: [
1096 {
1097 xtype: 'panel',
1098 region: 'center',
1099 layout: {
1100 type: 'vbox',
1101 align: 'stretch',
1102 },
1103 items: [
1104 {
1105 xtype: 'pveBackedGuests',
1106 flex: 1,
1107 layout: 'fit',
1108 store: not_backed_store,
1109 },
1110 ],
1111 },
1112 ],
1113 }).show();
1114 };
1115
1116 var edit_btn = new Proxmox.button.Button({
1117 text: gettext('Edit'),
1118 disabled: true,
1119 selModel: sm,
1120 handler: run_editor,
1121 });
1122
1123 var run_btn = new Proxmox.button.Button({
1124 text: gettext('Run now'),
1125 disabled: true,
1126 selModel: sm,
1127 handler: function() {
1128 var rec = sm.getSelection()[0];
1129 if (!rec) {
1130 return;
1131 }
1132
1133 Ext.Msg.show({
1134 title: gettext('Confirm'),
1135 icon: Ext.Msg.QUESTION,
1136 msg: gettext('Start the selected backup job now?'),
1137 buttons: Ext.Msg.YESNO,
1138 callback: function(btn) {
1139 if (btn !== 'yes') {
1140 return;
1141 }
1142 run_backup_now(rec.data);
1143 },
1144 });
1145 },
1146 });
1147
1148 var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
1149 selModel: sm,
1150 baseurl: '/cluster/backup',
1151 callback: function() {
1152 reload();
1153 },
1154 });
1155
1156 var detail_btn = new Proxmox.button.Button({
1157 text: gettext('Job Detail'),
1158 disabled: true,
1159 tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'),
1160 selModel: sm,
1161 handler: run_detail,
1162 });
1163
1164 noBackupJobWarning = Ext.create('Ext.toolbar.TextItem', {
1165 html: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
1166 hidden: true,
1167 });
1168
1169 noBackupJobInfoButton = new Proxmox.button.Button({
1170 text: gettext('Show'),
1171 hidden: true,
1172 handler: run_show_not_backed,
1173 });
1174
1175 Proxmox.Utils.monStoreErrors(me, store);
1176
1177 Ext.apply(me, {
1178 store: store,
1179 selModel: sm,
1180 stateful: true,
1181 stateId: 'grid-dc-backup',
1182 viewConfig: {
1183 trackOver: false,
1184 },
1185 tbar: [
1186 {
1187 text: gettext('Add'),
1188 handler: function() {
1189 var win = Ext.create('PVE.dc.BackupEdit', {});
1190 win.on('destroy', reload);
1191 win.show();
1192 },
1193 },
1194 '-',
1195 remove_btn,
1196 edit_btn,
1197 detail_btn,
1198 '-',
1199 run_btn,
1200 '->',
1201 noBackupJobWarning,
1202 noBackupJobInfoButton,
1203 ],
1204 columns: [
1205 {
1206 header: gettext('Enabled'),
1207 width: 80,
1208 dataIndex: 'enabled',
1209 xtype: 'checkcolumn',
1210 sortable: true,
1211 disabled: true,
1212 disabledCls: 'x-item-enabled',
1213 stopSelection: false,
1214 },
1215 {
1216 header: gettext('Node'),
1217 width: 100,
1218 sortable: true,
1219 dataIndex: 'node',
1220 renderer: function(value) {
1221 if (value) {
1222 return value;
1223 }
1224 return me.allText;
1225 },
1226 },
1227 {
1228 header: gettext('Day of week'),
1229 width: 200,
1230 sortable: false,
1231 dataIndex: 'dow',
1232 renderer: PVE.Utils.render_backup_days_of_week,
1233 },
1234 {
1235 header: gettext('Start Time'),
1236 width: 60,
1237 sortable: true,
1238 dataIndex: 'starttime',
1239 },
1240 {
1241 header: gettext('Storage'),
1242 width: 100,
1243 sortable: true,
1244 dataIndex: 'storage',
1245 },
1246 {
1247 header: gettext('Selection'),
1248 flex: 1,
1249 sortable: false,
1250 dataIndex: 'vmid',
1251 renderer: PVE.Utils.render_backup_selection,
1252 },
1253 ],
1254 listeners: {
1255 activate: reload,
1256 itemdblclick: run_editor,
1257 },
1258 });
1259
1260 me.callParent();
1261 },
1262 }, function() {
1263 Ext.define('pve-cluster-backup', {
1264 extend: 'Ext.data.Model',
1265 fields: [
1266 'id',
1267 'compress',
1268 'dow',
1269 'exclude',
1270 'mailto',
1271 'mode',
1272 'node',
1273 'pool',
1274 'starttime',
1275 'storage',
1276 'vmid',
1277 { name: 'enabled', type: 'boolean' },
1278 { name: 'all', type: 'boolean' },
1279 ],
1280 });
1281 });