]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/Backup.js
ui: backup info: make initial height dependent of body view-height
[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 let sel = selected.map(record => record.data.vmid);
44 // to avoid endless recursion suspend the vmidField change
45 // event temporary as it calls us again
46 vmidField.suspendEvent('change');
47 vmidField.setValue(sel);
48 vmidField.resumeEvent('change');
49 },
50 },
51 });
52
53 let storagesel = Ext.create('PVE.form.StorageSelector', {
54 fieldLabel: gettext('Storage'),
55 clusterView: true,
56 storageContent: 'backup',
57 allowBlank: false,
58 name: 'storage',
59 listeners: {
60 change: function(f, v) {
61 let store = f.getStore();
62 let rec = store.findRecord('storage', v, 0, false, true, true);
63 let compressionSelector = me.down('pveCompressionSelector');
64
65 if (rec && rec.data && rec.data.type === 'pbs') {
66 compressionSelector.setValue('zstd');
67 compressionSelector.setDisabled(true);
68 } else if (!compressionSelector.getEditable()) {
69 compressionSelector.setDisabled(false);
70 }
71 },
72 },
73 });
74
75 let store = new Ext.data.Store({
76 model: 'PVEResources',
77 sorters: {
78 property: 'vmid',
79 direction: 'ASC',
80 },
81 });
82
83 let vmgrid = Ext.createWidget('grid', {
84 store: store,
85 border: true,
86 height: 300,
87 selModel: sm,
88 disabled: true,
89 columns: [
90 {
91 header: 'ID',
92 dataIndex: 'vmid',
93 width: 60,
94 },
95 {
96 header: gettext('Node'),
97 dataIndex: 'node',
98 },
99 {
100 header: gettext('Status'),
101 dataIndex: 'uptime',
102 renderer: function(value) {
103 if (value) {
104 return Proxmox.Utils.runningText;
105 } else {
106 return Proxmox.Utils.stoppedText;
107 }
108 },
109 },
110 {
111 header: gettext('Name'),
112 dataIndex: 'name',
113 flex: 1,
114 },
115 {
116 header: gettext('Type'),
117 dataIndex: 'type',
118 },
119 ],
120 });
121
122 let selectPoolMembers = function(poolid) {
123 if (!poolid) {
124 return;
125 }
126 sm.deselectAll(true);
127 store.filter([
128 {
129 id: 'poolFilter',
130 property: 'pool',
131 value: poolid,
132 },
133 ]);
134 sm.selectAll(true);
135 };
136
137 let selPool = Ext.create('PVE.form.PoolSelector', {
138 fieldLabel: gettext('Pool to backup'),
139 hidden: true,
140 allowBlank: true,
141 name: 'pool',
142 listeners: {
143 change: function(selpool, newValue, oldValue) {
144 selectPoolMembers(newValue);
145 },
146 },
147 });
148
149 let nodesel = Ext.create('PVE.form.NodeSelector', {
150 name: 'node',
151 fieldLabel: gettext('Node'),
152 allowBlank: true,
153 editable: true,
154 autoSelect: false,
155 emptyText: '-- ' + gettext('All') + ' --',
156 listeners: {
157 change: function(f, value) {
158 storagesel.setNodename(value);
159 let mode = selModeField.getValue();
160 store.clearFilter();
161 store.filterBy(function(rec) {
162 return !value || rec.get('node') === value;
163 });
164 if (mode === 'all') {
165 sm.selectAll(true);
166 }
167 if (mode === 'pool') {
168 selectPoolMembers(selPool.value);
169 }
170 },
171 },
172 });
173
174 let column1 = [
175 nodesel,
176 storagesel,
177 {
178 xtype: 'pveCalendarEvent',
179 fieldLabel: gettext('Schedule'),
180 allowBlank: false,
181 name: 'schedule',
182 },
183 selModeField,
184 selPool,
185 ];
186
187 let column2 = [
188 {
189 xtype: 'textfield',
190 fieldLabel: gettext('Send email to'),
191 name: 'mailto',
192 },
193 {
194 xtype: 'pveEmailNotificationSelector',
195 fieldLabel: gettext('Email'),
196 name: 'mailnotification',
197 deleteEmpty: !me.isCreate,
198 value: me.isCreate ? 'always' : '',
199 },
200 {
201 xtype: 'pveCompressionSelector',
202 fieldLabel: gettext('Compression'),
203 name: 'compress',
204 deleteEmpty: !me.isCreate,
205 value: 'zstd',
206 },
207 {
208 xtype: 'pveBackupModeSelector',
209 fieldLabel: gettext('Mode'),
210 value: 'snapshot',
211 name: 'mode',
212 },
213 {
214 xtype: 'proxmoxcheckbox',
215 fieldLabel: gettext('Enable'),
216 name: 'enabled',
217 uncheckedValue: 0,
218 defaultValue: 1,
219 checked: true,
220 },
221 vmidField,
222 ];
223
224 let ipanel = Ext.create('Proxmox.panel.InputPanel', {
225 onlineHelp: 'chapter_vzdump',
226 column1: column1,
227 column2: column2,
228 columnB: [
229 {
230 xtype: 'proxmoxtextfield',
231 name: 'comment',
232 fieldLabel: gettext('Job Comment'),
233 deleteEmpty: !me.isCreate,
234 autoEl: {
235 tag: 'div',
236 'data-qtip': gettext('Description of the job'),
237 },
238 },
239 vmgrid,
240 ],
241 advancedColumn1: [
242 {
243 xtype: 'proxmoxcheckbox',
244 fieldLabel: gettext('Repeat missed'),
245 name: 'repeat-missed',
246 uncheckedValue: 0,
247 defaultValue: 0,
248 deleteDefaultValue: !me.isCreate,
249 },
250 ],
251 onGetValues: function(values) {
252 if (!values.node) {
253 if (!me.isCreate) {
254 Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
255 }
256 delete values.node;
257 }
258
259 if (!values.id && me.isCreate) {
260 values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
261 }
262
263 let selMode = values.selMode;
264 delete values.selMode;
265
266 if (selMode === 'all') {
267 values.all = 1;
268 values.exclude = '';
269 delete values.vmid;
270 } else if (selMode === 'exclude') {
271 values.all = 1;
272 values.exclude = values.vmid;
273 delete values.vmid;
274 } else if (selMode === 'pool') {
275 delete values.vmid;
276 }
277
278 if (selMode !== 'pool') {
279 delete values.pool;
280 }
281 return values;
282 },
283 });
284
285 let update_vmid_selection = function(list, mode) {
286 if (mode !== 'all' && mode !== 'pool') {
287 sm.deselectAll(true);
288 if (list) {
289 Ext.Array.each(list.split(','), function(vmid) {
290 var rec = store.findRecord('vmid', vmid, 0, false, true, true);
291 if (rec) {
292 sm.select(rec, true);
293 }
294 });
295 }
296 }
297 };
298
299 vmidField.on('change', function(f, value) {
300 let mode = selModeField.getValue();
301 update_vmid_selection(value, mode);
302 });
303
304 selModeField.on('change', function(f, value, oldValue) {
305 if (oldValue === 'pool') {
306 store.removeFilter('poolFilter');
307 }
308
309 if (oldValue === 'all') {
310 sm.deselectAll(true);
311 vmidField.setValue('');
312 }
313
314 if (value === 'all') {
315 sm.selectAll(true);
316 vmgrid.setDisabled(true);
317 } else {
318 vmgrid.setDisabled(false);
319 }
320
321 if (value === 'pool') {
322 vmgrid.setDisabled(true);
323 vmidField.setValue('');
324 selPool.setVisible(true);
325 selPool.setDisabled(false);
326 selPool.allowBlank = false;
327 selectPoolMembers(selPool.value);
328 } else {
329 selPool.setVisible(false);
330 selPool.setDisabled(true);
331 selPool.allowBlank = true;
332 }
333 let list = vmidField.getValue();
334 update_vmid_selection(list, value);
335 });
336
337 let reload = function() {
338 store.load({
339 params: {
340 type: 'vm',
341 },
342 callback: function() {
343 let node = nodesel.getValue();
344 store.clearFilter();
345 store.filterBy(rec => !node || node.length === 0 || rec.get('node') === node);
346 let list = vmidField.getValue();
347 let mode = selModeField.getValue();
348 if (mode === 'all') {
349 sm.selectAll(true);
350 } else if (mode === 'pool') {
351 selectPoolMembers(selPool.value);
352 } else {
353 update_vmid_selection(list, mode);
354 }
355 },
356 });
357 };
358
359 Ext.applyIf(me, {
360 subject: gettext("Backup Job"),
361 url: url,
362 method: method,
363 bodyPadding: 0,
364 items: [
365 {
366 xtype: 'tabpanel',
367 region: 'center',
368 layout: 'fit',
369 bodyPadding: 10,
370 items: [
371 {
372 xtype: 'container',
373 title: gettext('General'),
374 region: 'center',
375 layout: {
376 type: 'vbox',
377 align: 'stretch',
378 },
379 items: [
380 ipanel,
381 ],
382 },
383 {
384 xtype: 'pveBackupJobPrunePanel',
385 title: gettext('Retention'),
386 isCreate: me.isCreate,
387 keepAllDefaultForCreate: false,
388 showPBSHint: false,
389 fallbackHintHtml: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
390 },
391 {
392 xtype: 'inputpanel',
393 title: gettext('Note Template'),
394 region: 'center',
395 layout: {
396 type: 'vbox',
397 align: 'stretch',
398 },
399 onGetValues: function(values) {
400 if (values['notes-template']) {
401 values['notes-template'] = PVE.Utils.escapeNotesTemplate(
402 values['notes-template']);
403 }
404 return values;
405 },
406 items: [
407 {
408 xtype: 'textarea',
409 name: 'notes-template',
410 fieldLabel: gettext('Backup Notes'),
411 height: 100,
412 maxLength: 512,
413 deleteEmpty: !me.isCreate,
414 value: me.isCreate ? '{{guestname}}' : undefined,
415 },
416 {
417 xtype: 'box',
418 style: {
419 margin: '8px 0px',
420 'line-height': '1.5em',
421 },
422 html: gettext('The notes are added to each backup created by this job.')
423 + '<br>'
424 + Ext.String.format(
425 gettext('Possible template variables are: {0}'),
426 PVE.Utils.notesTemplateVars.map(v => `<code>{{${v}}}</code>`).join(', '),
427 ),
428 },
429 ],
430 },
431 ],
432 },
433 ],
434
435 });
436
437 me.callParent();
438
439 if (me.isCreate) {
440 selModeField.setValue('include');
441 } else {
442 me.load({
443 success: function(response, options) {
444 let data = response.result.data;
445
446 data.dow = (data.dow || '').split(',');
447
448 if (data.all || data.exclude) {
449 if (data.exclude) {
450 data.vmid = data.exclude;
451 data.selMode = 'exclude';
452 } else {
453 data.vmid = '';
454 data.selMode = 'all';
455 }
456 } else if (data.pool) {
457 data.selMode = 'pool';
458 data.selPool = data.pool;
459 } else {
460 data.selMode = 'include';
461 }
462
463 if (data['prune-backups']) {
464 Object.assign(data, data['prune-backups']);
465 delete data['prune-backups'];
466 } else if (data.maxfiles !== undefined) {
467 if (data.maxfiles > 0) {
468 data['keep-last'] = data.maxfiles;
469 } else {
470 data['keep-all'] = 1;
471 }
472 delete data.maxfiles;
473 }
474
475 if (data['notes-template']) {
476 data['notes-template'] = PVE.Utils.unEscapeNotesTemplate(
477 data['notes-template']);
478 }
479
480 me.setValues(data);
481 },
482 });
483 }
484
485 reload();
486 },
487 });
488
489 Ext.define('PVE.dc.BackupView', {
490 extend: 'Ext.grid.GridPanel',
491
492 alias: ['widget.pveDcBackupView'],
493
494 onlineHelp: 'chapter_vzdump',
495
496 allText: '-- ' + gettext('All') + ' --',
497
498 initComponent: function() {
499 let me = this;
500
501 let store = new Ext.data.Store({
502 model: 'pve-cluster-backup',
503 proxy: {
504 type: 'proxmox',
505 url: "/api2/json/cluster/backup",
506 },
507 });
508
509 let not_backed_store = new Ext.data.Store({
510 sorters: 'vmid',
511 proxy: {
512 type: 'proxmox',
513 url: 'api2/json/cluster/backup-info/not-backed-up',
514 },
515 });
516
517 let noBackupJobWarning, noBackupJobInfoButton;
518 let reload = function() {
519 store.load();
520 not_backed_store.load({
521 callback: function(records, operation, success) {
522 noBackupJobWarning.setVisible(records.length > 0);
523 noBackupJobInfoButton.setVisible(records.length > 0);
524 },
525 });
526 };
527
528 let sm = Ext.create('Ext.selection.RowModel', {});
529
530 let run_editor = function() {
531 let rec = sm.getSelection()[0];
532 if (!rec) {
533 return;
534 }
535
536 let win = Ext.create('PVE.dc.BackupEdit', {
537 jobid: rec.data.id,
538 });
539 win.on('destroy', reload);
540 win.show();
541 };
542
543 let run_detail = function() {
544 let record = sm.getSelection()[0];
545 if (!record) {
546 return;
547 }
548 Ext.create('Ext.window.Window', {
549 modal: true,
550 width: 800,
551 height: Ext.getBody().getViewSize().height > 1000 ? 800 : 600, // factor out as common infra?
552 resizable: true,
553 layout: 'fit',
554 title: gettext('Backup Details'),
555 items: [
556 {
557 xtype: 'panel',
558 region: 'center',
559 layout: {
560 type: 'vbox',
561 align: 'stretch',
562 },
563 items: [
564 {
565 xtype: 'pveBackupInfo',
566 flex: 0,
567 layout: 'fit',
568 record: record.data,
569 },
570 {
571 xtype: 'pveBackupDiskTree',
572 title: gettext('Included disks'),
573 flex: 1,
574 jobid: record.data.id,
575 },
576 ],
577 },
578 ],
579 }).show();
580 };
581
582 let run_backup_now = function(job) {
583 job = Ext.clone(job);
584
585 let jobNode = job.node;
586 // Remove properties related to scheduling
587 delete job.enabled;
588 delete job.starttime;
589 delete job.dow;
590 delete job.id;
591 delete job.schedule;
592 delete job.type;
593 delete job.node;
594 delete job.comment;
595 delete job['next-run'];
596 delete job['repeat-missed'];
597 job.all = job.all === true ? 1 : 0;
598
599 ['performance', 'prune-backups'].forEach(key => {
600 if (job[key]) {
601 job[key] = PVE.Parser.printPropertyString(job[key]);
602 }
603 });
604
605 let allNodes = PVE.data.ResourceStore.getNodes();
606 let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
607 let errors = [];
608
609 if (jobNode !== undefined) {
610 if (!nodes.includes(jobNode)) {
611 Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
612 return;
613 }
614 nodes = [jobNode];
615 } else {
616 let unkownNodes = allNodes.filter(node => node.status !== 'online');
617 if (unkownNodes.length > 0) {errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));}
618 }
619 let jobTotalCount = nodes.length, jobsStarted = 0;
620
621 Ext.Msg.show({
622 title: gettext('Please wait...'),
623 closable: false,
624 progress: true,
625 progressText: '0/' + jobTotalCount,
626 });
627
628 let postRequest = function() {
629 jobsStarted++;
630 Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
631
632 if (jobsStarted === jobTotalCount) {
633 Ext.Msg.hide();
634 if (errors.length > 0) {
635 Ext.Msg.alert('Error', 'Some errors have been encountered:<br />' + errors.join('<br />'));
636 }
637 }
638 };
639
640 nodes.forEach(node => Proxmox.Utils.API2Request({
641 url: '/nodes/' + node + '/vzdump',
642 method: 'POST',
643 params: job,
644 failure: function(response, opts) {
645 errors.push(node + ': ' + response.htmlStatus);
646 postRequest();
647 },
648 success: postRequest,
649 }));
650 };
651
652 let run_show_not_backed = function() {
653 Ext.create('Ext.window.Window', {
654 modal: true,
655 width: 600,
656 height: 500,
657 resizable: true,
658 layout: 'fit',
659 title: gettext('Guests without backup job'),
660 items: [
661 {
662 xtype: 'panel',
663 region: 'center',
664 layout: {
665 type: 'vbox',
666 align: 'stretch',
667 },
668 items: [
669 {
670 xtype: 'pveBackedGuests',
671 flex: 1,
672 layout: 'fit',
673 store: not_backed_store,
674 },
675 ],
676 },
677 ],
678 }).show();
679 };
680
681 var edit_btn = new Proxmox.button.Button({
682 text: gettext('Edit'),
683 disabled: true,
684 selModel: sm,
685 handler: run_editor,
686 });
687
688 var run_btn = new Proxmox.button.Button({
689 text: gettext('Run now'),
690 disabled: true,
691 selModel: sm,
692 handler: function() {
693 var rec = sm.getSelection()[0];
694 if (!rec) {
695 return;
696 }
697
698 Ext.Msg.show({
699 title: gettext('Confirm'),
700 icon: Ext.Msg.QUESTION,
701 msg: gettext('Start the selected backup job now?'),
702 buttons: Ext.Msg.YESNO,
703 callback: function(btn) {
704 if (btn !== 'yes') {
705 return;
706 }
707 run_backup_now(rec.data);
708 },
709 });
710 },
711 });
712
713 var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
714 selModel: sm,
715 baseurl: '/cluster/backup',
716 callback: function() {
717 reload();
718 },
719 });
720
721 var detail_btn = new Proxmox.button.Button({
722 text: gettext('Job Detail'),
723 disabled: true,
724 tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'),
725 selModel: sm,
726 handler: run_detail,
727 });
728
729 noBackupJobWarning = Ext.create('Ext.toolbar.TextItem', {
730 html: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
731 hidden: true,
732 });
733
734 noBackupJobInfoButton = new Proxmox.button.Button({
735 text: gettext('Show'),
736 hidden: true,
737 handler: run_show_not_backed,
738 });
739
740 Proxmox.Utils.monStoreErrors(me, store);
741
742 Ext.apply(me, {
743 store: store,
744 selModel: sm,
745 stateful: true,
746 stateId: 'grid-dc-backup',
747 viewConfig: {
748 trackOver: false,
749 },
750 tbar: [
751 {
752 text: gettext('Add'),
753 handler: function() {
754 var win = Ext.create('PVE.dc.BackupEdit', {});
755 win.on('destroy', reload);
756 win.show();
757 },
758 },
759 '-',
760 remove_btn,
761 edit_btn,
762 detail_btn,
763 '-',
764 run_btn,
765 '->',
766 noBackupJobWarning,
767 noBackupJobInfoButton,
768 '-',
769 {
770 xtype: 'proxmoxButton',
771 selModel: null,
772 text: gettext('Schedule Simulator'),
773 handler: () => {
774 let record = sm.getSelection()[0];
775 let schedule;
776 if (record) {
777 schedule = record.data.schedule;
778 }
779 Ext.create('PVE.window.ScheduleSimulator', {
780 schedule,
781 }).show();
782 },
783 },
784 ],
785 columns: [
786 {
787 header: gettext('Enabled'),
788 width: 80,
789 dataIndex: 'enabled',
790 xtype: 'checkcolumn',
791 sortable: true,
792 disabled: true,
793 disabledCls: 'x-item-enabled',
794 stopSelection: false,
795 },
796 {
797 header: gettext('ID'),
798 dataIndex: 'id',
799 hidden: true,
800 },
801 {
802 header: gettext('Node'),
803 width: 100,
804 sortable: true,
805 dataIndex: 'node',
806 renderer: function(value) {
807 if (value) {
808 return value;
809 }
810 return me.allText;
811 },
812 },
813 {
814 header: gettext('Schedule'),
815 width: 150,
816 dataIndex: 'schedule',
817 },
818 {
819 text: gettext('Next Run'),
820 dataIndex: 'next-run',
821 width: 150,
822 renderer: PVE.Utils.render_next_event,
823 },
824 {
825 header: gettext('Storage'),
826 width: 100,
827 sortable: true,
828 dataIndex: 'storage',
829 },
830 {
831 header: gettext('Comment'),
832 dataIndex: 'comment',
833 renderer: Ext.htmlEncode,
834 sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''),
835 flex: 1,
836 },
837 {
838 header: gettext('Retention'),
839 dataIndex: 'prune-backups',
840 renderer: v => v ? PVE.Parser.printPropertyString(v) : gettext('Fallback from storage config'),
841 flex: 2,
842 },
843 {
844 header: gettext('Selection'),
845 flex: 4,
846 sortable: false,
847 dataIndex: 'vmid',
848 renderer: PVE.Utils.render_backup_selection,
849 },
850 ],
851 listeners: {
852 activate: reload,
853 itemdblclick: run_editor,
854 },
855 });
856
857 me.callParent();
858 },
859 }, function() {
860 Ext.define('pve-cluster-backup', {
861 extend: 'Ext.data.Model',
862 fields: [
863 'id',
864 'compress',
865 'dow',
866 'exclude',
867 'mailto',
868 'mode',
869 'node',
870 'pool',
871 'prune-backups',
872 'starttime',
873 'storage',
874 'vmid',
875 { name: 'enabled', type: 'boolean' },
876 { name: 'all', type: 'boolean' },
877 ],
878 });
879 });