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