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