]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/Backup.js
ui: rename pveEditPruneInputPanel to pveBackupJobPrunePanel
[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 padding: '5 0 5 10',
593
594 column1: [
595 {
596 name: 'node',
597 fieldLabel: gettext('Node'),
598 xtype: 'displayfield',
599 renderer: function(value) {
600 if (!value) {
601 return '-- ' + gettext('All') + ' --';
602 } else {
603 return value;
604 }
605 },
606 },
607 {
608 name: 'storage',
609 fieldLabel: gettext('Storage'),
610 xtype: 'displayfield',
611 },
612 {
613 name: 'dow',
614 fieldLabel: gettext('Day of week'),
615 xtype: 'displayfield',
616 renderer: PVE.Utils.render_backup_days_of_week,
617 },
618 {
619 name: 'starttime',
620 fieldLabel: gettext('Start Time'),
621 xtype: 'displayfield',
622 },
623 {
624 name: 'selMode',
625 fieldLabel: gettext('Selection mode'),
626 xtype: 'displayfield',
627 },
628 {
629 name: 'pool',
630 fieldLabel: gettext('Pool to backup'),
631 xtype: 'displayfield',
632 },
633 ],
634 column2: [
635 {
636 name: 'mailto',
637 fieldLabel: gettext('Send email to'),
638 xtype: 'displayfield',
639 },
640 {
641 name: 'mailnotification',
642 fieldLabel: gettext('Email notification'),
643 xtype: 'displayfield',
644 renderer: function(value) {
645 let msg;
646 switch (value) {
647 case 'always':
648 msg = gettext('Always');
649 break;
650 case 'failure':
651 msg = gettext('On failure only');
652 break;
653 }
654 return msg;
655 },
656 },
657 {
658 name: 'compress',
659 fieldLabel: gettext('Compression'),
660 xtype: 'displayfield',
661 },
662 {
663 name: 'mode',
664 fieldLabel: gettext('Mode'),
665 xtype: 'displayfield',
666 renderer: function(value) {
667 let msg;
668 switch (value) {
669 case 'snapshot':
670 msg = gettext('Snapshot');
671 break;
672 case 'suspend':
673 msg = gettext('Suspend');
674 break;
675 case 'stop':
676 msg = gettext('Stop');
677 break;
678 }
679 return msg;
680 },
681 },
682 {
683 name: 'enabled',
684 fieldLabel: gettext('Enabled'),
685 xtype: 'displayfield',
686 renderer: function(value) {
687 if (PVE.Parser.parseBoolean(value.toString())) {
688 return gettext('Yes');
689 } else {
690 return gettext('No');
691 }
692 },
693 },
694 ],
695
696 setValues: function(values) {
697 var me = this;
698
699 Ext.iterate(values, function(fieldId, val) {
700 let field = me.query('[isFormField][name=' + fieldId + ']')[0];
701 if (field) {
702 field.setValue(val);
703 }
704 });
705
706 // selection Mode depends on the presence/absence of several keys
707 let selModeField = me.query('[isFormField][name=selMode]')[0];
708 let selMode = 'none';
709 if (values.vmid) {
710 selMode = gettext('Include selected VMs');
711 }
712 if (values.all) {
713 selMode = gettext('All');
714 }
715 if (values.exclude) {
716 selMode = gettext('Exclude selected VMs');
717 }
718 if (values.pool) {
719 selMode = gettext('Pool based');
720 }
721 selModeField.setValue(selMode);
722
723 if (!values.pool) {
724 let poolField = me.query('[isFormField][name=pool]')[0];
725 poolField.setVisible(0);
726 }
727 },
728
729 initComponent: function() {
730 var me = this;
731
732 if (!me.record) {
733 throw "no data provided";
734 }
735 me.callParent();
736
737 me.setValues(me.record);
738 },
739 });
740
741
742 Ext.define('PVE.dc.BackedGuests', {
743 extend: 'Ext.grid.GridPanel',
744 alias: 'widget.pveBackedGuests',
745
746 textfilter: '',
747
748 columns: [
749 {
750 header: gettext('Type'),
751 dataIndex: "type",
752 renderer: PVE.Utils.render_resource_type,
753 flex: 1,
754 sortable: true,
755 },
756 {
757 header: gettext('VMID'),
758 dataIndex: 'vmid',
759 flex: 1,
760 sortable: true,
761 },
762 {
763 header: gettext('Name'),
764 dataIndex: 'name',
765 flex: 2,
766 sortable: true,
767 },
768 ],
769
770 initComponent: function() {
771 let me = this;
772
773 me.store.clearFilter(true);
774
775 Ext.apply(me, {
776 stateful: true,
777 stateId: 'grid-dc-backed-guests',
778 tbar: [
779 '->',
780 gettext('Search') + ':', ' ',
781 {
782 xtype: 'textfield',
783 width: 200,
784 emptyText: 'Name, VMID, Type',
785 enableKeyEvents: true,
786 listeners: {
787 buffer: 500,
788 keyup: function(field) {
789 let searchValue = field.getValue().toLowerCase();
790 me.store.clearFilter(true);
791 me.store.filterBy(function(record) {
792 let data = record.data;
793 for (const property in ['name', 'id', 'type']) {
794 if (data[property] === null) {
795 continue;
796 }
797 let v = data[property].toString();
798 if (v !== undefined) {
799 v = v.toLowerCase();
800 if (v.includes(searchValue)) {
801 return true;
802 }
803 }
804 }
805 return false;
806 });
807 },
808 },
809 },
810 ],
811 viewConfig: {
812 stripeRows: true,
813 trackOver: false,
814 },
815 });
816 me.callParent();
817 },
818 });
819
820 Ext.define('PVE.dc.BackupView', {
821 extend: 'Ext.grid.GridPanel',
822
823 alias: ['widget.pveDcBackupView'],
824
825 onlineHelp: 'chapter_vzdump',
826
827 allText: '-- ' + gettext('All') + ' --',
828
829 initComponent: function() {
830 let me = this;
831
832 let store = new Ext.data.Store({
833 model: 'pve-cluster-backup',
834 proxy: {
835 type: 'proxmox',
836 url: "/api2/json/cluster/backup",
837 },
838 });
839
840 let not_backed_store = new Ext.data.Store({
841 sorters: 'vmid',
842 proxy: {
843 type: 'proxmox',
844 url: 'api2/json/cluster/backup-info/not-backed-up',
845 },
846 });
847
848 let noBackupJobWarning, noBackupJobInfoButton;
849 let reload = function() {
850 store.load();
851 not_backed_store.load({
852 callback: function(records, operation, success) {
853 noBackupJobWarning.setVisible(records.length > 0);
854 noBackupJobInfoButton.setVisible(records.length > 0);
855 },
856 });
857 };
858
859 let sm = Ext.create('Ext.selection.RowModel', {});
860
861 let run_editor = function() {
862 let rec = sm.getSelection()[0];
863 if (!rec) {
864 return;
865 }
866
867 let win = Ext.create('PVE.dc.BackupEdit', {
868 jobid: rec.data.id,
869 });
870 win.on('destroy', reload);
871 win.show();
872 };
873
874 let run_detail = function() {
875 let record = sm.getSelection()[0];
876 if (!record) {
877 return;
878 }
879 Ext.create('Ext.window.Window', {
880 modal: true,
881 width: 800,
882 height: 600,
883 stateful: true,
884 stateId: 'backup-detail-view',
885 resizable: true,
886 layout: 'fit',
887 title: gettext('Backup Details'),
888 items: [
889 {
890 xtype: 'panel',
891 region: 'center',
892 layout: {
893 type: 'vbox',
894 align: 'stretch',
895 },
896 items: [
897 {
898 xtype: 'pveBackupInfo',
899 flex: 0,
900 layout: 'fit',
901 record: record.data,
902 },
903 {
904 xtype: 'pveBackupDiskTree',
905 title: gettext('Included disks'),
906 flex: 1,
907 jobid: record.data.id,
908 },
909 ],
910 },
911 ],
912 }).show();
913 };
914
915 let run_backup_now = function(job) {
916 job = Ext.clone(job);
917
918 let jobNode = job.node;
919 // Remove properties related to scheduling
920 delete job.enabled;
921 delete job.starttime;
922 delete job.dow;
923 delete job.id;
924 delete job.node;
925 job.all = job.all === true ? 1 : 0;
926
927 if (job['prune-backups']) {
928 job['prune-backups'] = PVE.Parser.printPropertyString(job['prune-backups']);
929 }
930
931 let allNodes = PVE.data.ResourceStore.getNodes();
932 let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
933 let errors = [];
934
935 if (jobNode !== undefined) {
936 if (!nodes.includes(jobNode)) {
937 Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
938 return;
939 }
940 nodes = [jobNode];
941 } else {
942 let unkownNodes = allNodes.filter(node => node.status !== 'online');
943 if (unkownNodes.length > 0) {errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));}
944 }
945 let jobTotalCount = nodes.length, jobsStarted = 0;
946
947 Ext.Msg.show({
948 title: gettext('Please wait...'),
949 closable: false,
950 progress: true,
951 progressText: '0/' + jobTotalCount,
952 });
953
954 let postRequest = function() {
955 jobsStarted++;
956 Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
957
958 if (jobsStarted === jobTotalCount) {
959 Ext.Msg.hide();
960 if (errors.length > 0) {
961 Ext.Msg.alert('Error', 'Some errors have been encountered:<br />' + errors.join('<br />'));
962 }
963 }
964 };
965
966 nodes.forEach(node => Proxmox.Utils.API2Request({
967 url: '/nodes/' + node + '/vzdump',
968 method: 'POST',
969 params: job,
970 failure: function(response, opts) {
971 errors.push(node + ': ' + response.htmlStatus);
972 postRequest();
973 },
974 success: postRequest,
975 }));
976 };
977
978 let run_show_not_backed = function() {
979 Ext.create('Ext.window.Window', {
980 modal: true,
981 width: 600,
982 height: 500,
983 resizable: true,
984 layout: 'fit',
985 title: gettext('Guests without backup job'),
986 items: [
987 {
988 xtype: 'panel',
989 region: 'center',
990 layout: {
991 type: 'vbox',
992 align: 'stretch',
993 },
994 items: [
995 {
996 xtype: 'pveBackedGuests',
997 flex: 1,
998 layout: 'fit',
999 store: not_backed_store,
1000 },
1001 ],
1002 },
1003 ],
1004 }).show();
1005 };
1006
1007 var edit_btn = new Proxmox.button.Button({
1008 text: gettext('Edit'),
1009 disabled: true,
1010 selModel: sm,
1011 handler: run_editor,
1012 });
1013
1014 var run_btn = new Proxmox.button.Button({
1015 text: gettext('Run now'),
1016 disabled: true,
1017 selModel: sm,
1018 handler: function() {
1019 var rec = sm.getSelection()[0];
1020 if (!rec) {
1021 return;
1022 }
1023
1024 Ext.Msg.show({
1025 title: gettext('Confirm'),
1026 icon: Ext.Msg.QUESTION,
1027 msg: gettext('Start the selected backup job now?'),
1028 buttons: Ext.Msg.YESNO,
1029 callback: function(btn) {
1030 if (btn !== 'yes') {
1031 return;
1032 }
1033 run_backup_now(rec.data);
1034 },
1035 });
1036 },
1037 });
1038
1039 var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
1040 selModel: sm,
1041 baseurl: '/cluster/backup',
1042 callback: function() {
1043 reload();
1044 },
1045 });
1046
1047 var detail_btn = new Proxmox.button.Button({
1048 text: gettext('Job Detail'),
1049 disabled: true,
1050 tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'),
1051 selModel: sm,
1052 handler: run_detail,
1053 });
1054
1055 noBackupJobWarning = Ext.create('Ext.toolbar.TextItem', {
1056 html: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
1057 hidden: true,
1058 });
1059
1060 noBackupJobInfoButton = new Proxmox.button.Button({
1061 text: gettext('Show'),
1062 hidden: true,
1063 handler: run_show_not_backed,
1064 });
1065
1066 Proxmox.Utils.monStoreErrors(me, store);
1067
1068 Ext.apply(me, {
1069 store: store,
1070 selModel: sm,
1071 stateful: true,
1072 stateId: 'grid-dc-backup',
1073 viewConfig: {
1074 trackOver: false,
1075 },
1076 tbar: [
1077 {
1078 text: gettext('Add'),
1079 handler: function() {
1080 var win = Ext.create('PVE.dc.BackupEdit', {});
1081 win.on('destroy', reload);
1082 win.show();
1083 },
1084 },
1085 '-',
1086 remove_btn,
1087 edit_btn,
1088 detail_btn,
1089 '-',
1090 run_btn,
1091 '->',
1092 noBackupJobWarning,
1093 noBackupJobInfoButton,
1094 ],
1095 columns: [
1096 {
1097 header: gettext('Enabled'),
1098 width: 80,
1099 dataIndex: 'enabled',
1100 xtype: 'checkcolumn',
1101 sortable: true,
1102 disabled: true,
1103 disabledCls: 'x-item-enabled',
1104 stopSelection: false,
1105 },
1106 {
1107 header: gettext('Node'),
1108 width: 100,
1109 sortable: true,
1110 dataIndex: 'node',
1111 renderer: function(value) {
1112 if (value) {
1113 return value;
1114 }
1115 return me.allText;
1116 },
1117 },
1118 {
1119 header: gettext('Day of week'),
1120 width: 200,
1121 sortable: false,
1122 dataIndex: 'dow',
1123 renderer: PVE.Utils.render_backup_days_of_week,
1124 },
1125 {
1126 header: gettext('Start Time'),
1127 width: 60,
1128 sortable: true,
1129 dataIndex: 'starttime',
1130 },
1131 {
1132 header: gettext('Storage'),
1133 width: 100,
1134 sortable: true,
1135 dataIndex: 'storage',
1136 },
1137 {
1138 header: gettext('Selection'),
1139 flex: 1,
1140 sortable: false,
1141 dataIndex: 'vmid',
1142 renderer: PVE.Utils.render_backup_selection,
1143 },
1144 ],
1145 listeners: {
1146 activate: reload,
1147 itemdblclick: run_editor,
1148 },
1149 });
1150
1151 me.callParent();
1152 },
1153 }, function() {
1154 Ext.define('pve-cluster-backup', {
1155 extend: 'Ext.data.Model',
1156 fields: [
1157 'id',
1158 'compress',
1159 'dow',
1160 'exclude',
1161 'mailto',
1162 'mode',
1163 'node',
1164 'pool',
1165 'starttime',
1166 'storage',
1167 'vmid',
1168 { name: 'enabled', type: 'boolean' },
1169 { name: 'all', type: 'boolean' },
1170 ],
1171 });
1172 });