]> git.proxmox.com Git - pve-manager.git/blobdiff - www/manager6/dc/Backup.js
update shipped appliance info index
[pve-manager.git] / www / manager6 / dc / Backup.js
index 059d01c7480da9f793951ef4e843d7b09d8ce11d..4ba80b315172ea825037d7bae760a890dc01220e 100644 (file)
@@ -2,798 +2,510 @@ Ext.define('PVE.dc.BackupEdit', {
     extend: 'Proxmox.window.Edit',
     alias: ['widget.pveDcBackupEdit'],
 
-    defaultFocus: undefined,
-
-    initComponent : function() {
-         var me = this;
+    mixins: ['Proxmox.Mixin.CBind'],
 
-        me.isCreate = !me.jobid;
+    defaultFocus: undefined,
 
-       var url;
-       var method;
+    subject: gettext("Backup Job"),
+    width: 720,
+    bodyPadding: 0,
 
-       if (me.isCreate) {
-            url = '/api2/extjs/cluster/backup';
-            method = 'POST';
-        } else {
-            url = '/api2/extjs/cluster/backup/' + me.jobid;
-            method = 'PUT';
-        }
+    url: '/api2/extjs/cluster/backup',
+    method: 'POST',
+    isCreate: true,
 
-       var vmidField = Ext.create('Ext.form.field.Hidden', {
-           name: 'vmid'
-       });
+    cbindData: function() {
+       let me = this;
+       if (me.jobid) {
+           me.isCreate = false;
+           me.method = 'PUT';
+           me.url += `/${me.jobid}`;
+       }
+       return {};
+    },
 
-       // 'value' can be assigned a string or an array
-       var selModeField =  Ext.create('Proxmox.form.KVComboBox', {
-           xtype: 'proxmoxKVComboBox',
-           comboItems: [
-               ['include', gettext('Include selected VMs')],
-               ['all', gettext('All')],
-               ['exclude', gettext('Exclude selected VMs')],
-               ['pool', gettext('Pool based')]
-           ],
-           fieldLabel: gettext('Selection mode'),
-           name: 'selMode',
-           value: ''
-       });
+    controller: {
+       xclass: 'Ext.app.ViewController',
 
-       var sm = Ext.create('Ext.selection.CheckboxModel', {
-           mode: 'SIMPLE',
-           listeners: {
-               selectionchange: function(model, selected) {
-                   var sel = [];
-                   Ext.Array.each(selected, function(record) {
-                       sel.push(record.data.vmid);
-                   });
-
-                   // to avoid endless recursion suspend the vmidField change
-                   // event temporary as it calls us again
-                   vmidField.suspendEvent('change');
-                   vmidField.setValue(sel);
-                   vmidField.resumeEvent('change');
-               }
-           }
-       });
-
-       var storagesel = Ext.create('PVE.form.StorageSelector', {
-           fieldLabel: gettext('Storage'),
-           nodename: 'localhost',
-           storageContent: 'backup',
-           allowBlank: false,
-           name: 'storage',
-           listeners: {
-               change: function(f, v) {
-                   let store = f.getStore();
-                   let rec = store.findRecord('storage', v, 0, false, true, true);
-                   let compressionSelector = me.down('pveCompressionSelector');
-
-                   if (rec && rec.data && rec.data.type === 'pbs') {
-                       compressionSelector.setValue('zstd');
-                       compressionSelector.setDisabled(true);
-                   } else if (!compressionSelector.getEditable()) {
-                       compressionSelector.setDisabled(false);
-                   }
+       onGetValues: function(values) {
+           let me = this;
+           let isCreate = me.getView().isCreate;
+           if (!values.node) {
+               if (!isCreate) {
+                   Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
                }
+               delete values.node;
            }
-       });
 
-       var store = new Ext.data.Store({
-           model: 'PVEResources',
-           sorters: {
-               property: 'vmid',
-               order: 'ASC'
+           // Get rid of new-old parameters for notification settings.
+           // These should only be set for those selected few who ran
+           // pve-manager from pvetest.
+           if (!isCreate) {
+               Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-policy' });
+               Proxmox.Utils.assemble_field_data(values, { 'delete': 'notification-target' });
            }
-       });
-
-       var vmgrid = Ext.createWidget('grid', {
-           store: store,
-           border: true,
-           height: 300,
-           selModel: sm,
-           disabled: true,
-           columns: [
-               {
-                   header: 'ID',
-                   dataIndex: 'vmid',
-                   width: 60
-               },
-               {
-                   header: gettext('Node'),
-                   dataIndex: 'node'
-               },
-               {
-                   header: gettext('Status'),
-                   dataIndex: 'uptime',
-                   renderer: function(value) {
-                       if (value) {
-                           return Proxmox.Utils.runningText;
-                       } else {
-                           return Proxmox.Utils.stoppedText;
-                       }
-                   }
-               },
-               {
-                   header: gettext('Name'),
-                   dataIndex: 'name',
-                   flex: 1
-               },
-               {
-                   header: gettext('Type'),
-                   dataIndex: 'type'
-               }
-           ]
-       });
 
-       var selectPoolMembers = function(poolid) {
-           if (!poolid) {
-               return;
+           if (!values.id && isCreate) {
+               values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
            }
-           sm.deselectAll(true);
-           store.filter([
-               {
-                   id: 'poolFilter',
-                   property: 'pool',
-                   value: poolid
-               }
-           ]);
-           sm.selectAll(true);
-       };
 
-       var selPool = Ext.create('PVE.form.PoolSelector', {
-           fieldLabel: gettext('Pool to backup'),
-           hidden: true,
-           allowBlank: true,
-           name: 'pool',
-           listeners: {
-               change: function( selpool, newValue, oldValue) {
-                   selectPoolMembers(newValue);
-               }
+           let selMode = values.selMode;
+           delete values.selMode;
+
+           if (selMode === 'all') {
+               values.all = 1;
+               values.exclude = '';
+               delete values.vmid;
+           } else if (selMode === 'exclude') {
+               values.all = 1;
+               values.exclude = values.vmid;
+               delete values.vmid;
+           } else if (selMode === 'pool') {
+               delete values.vmid;
            }
-       });
 
-       var nodesel = Ext.create('PVE.form.NodeSelector', {
-           name: 'node',
-           fieldLabel: gettext('Node'),
-           allowBlank: true,
-           editable: true,
-           autoSelect: false,
-           emptyText: '-- ' + gettext('All') + ' --',
-           listeners: {
-               change: function(f, value) {
-                   storagesel.setNodename(value || 'localhost');
-                   var mode = selModeField.getValue();
-                   store.clearFilter();
-                   store.filterBy(function(rec) {
-                       return (!value || rec.get('node') === value);
-                   });
-                   if (mode === 'all') {
-                       sm.selectAll(true);
-                   }
-
-                   if (mode === 'pool') {
-                       selectPoolMembers(selPool.value);
-                   }
-               }
+           if (selMode !== 'pool') {
+               delete values.pool;
            }
-       });
+           return values;
+       },
 
-       var column1 = [
-           nodesel,
-           storagesel,
-           {
-               xtype: 'pveDayOfWeekSelector',
-               name: 'dow',
-               fieldLabel: gettext('Day of week'),
-               multiSelect: true,
-               value: ['sat'],
-               allowBlank: false
-           },
-           {
-               xtype: 'timefield',
-               fieldLabel: gettext('Start Time'),
-               name: 'starttime',
-               format: 'H:i',
-               formatText: 'HH:MM',
-               value: '00:00',
-               allowBlank: false
-           },
-           selModeField,
-           selPool
-       ];
-
-       var column2 = [
-           {
-               xtype: 'textfield',
-               fieldLabel: gettext('Send email to'),
-               name: 'mailto'
-           },
-           {
-               xtype: 'pveEmailNotificationSelector',
-               fieldLabel: gettext('Email notification'),
-               name: 'mailnotification',
-               deleteEmpty: me.isCreate ? false : true,
-               value: me.isCreate ? 'always' : ''
-           },
-           {
-               xtype: 'pveCompressionSelector',
-               fieldLabel: gettext('Compression'),
-               name: 'compress',
-               deleteEmpty: me.isCreate ? false : true,
-               value: 'zstd'
-           },
-           {
-               xtype: 'pveBackupModeSelector',
-               fieldLabel: gettext('Mode'),
-               value: 'snapshot',
-               name: 'mode'
-           },
-           {
-               xtype: 'proxmoxcheckbox',
-               fieldLabel: gettext('Enable'),
-               name: 'enabled',
-               uncheckedValue: 0,
-               defaultValue: 1,
-               checked: true
-           },
-           vmidField
-       ];
-
-       var ipanel = Ext.create('Proxmox.panel.InputPanel', {
-           onlineHelp: 'chapter_vzdump',
-           column1: column1,
-           column2:  column2,
-           onGetValues: function(values) {
-               if (!values.node) {
-                   if (!me.isCreate) {
-                       Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
-                   }
-                   delete values.node;
-               }
+       nodeChange: function(f, value) {
+           let me = this;
+           me.lookup('storageSelector').setNodename(value);
+           let vmgrid = me.lookup('vmgrid');
+           let store = vmgrid.getStore();
 
-               var selMode = values.selMode;
-               delete values.selMode;
-
-               if (selMode === 'all') {
-                   values.all = 1;
-                   values.exclude = '';
-                   delete values.vmid;
-               } else if (selMode === 'exclude') {
-                   values.all = 1;
-                   values.exclude = values.vmid;
-                   delete values.vmid;
-               } else if (selMode === 'pool') {
-                   delete values.vmid;
-               }
+           store.clearFilter();
+           store.filterBy(function(rec) {
+               return !value || rec.get('node') === value;
+           });
 
-               if (selMode !== 'pool') {
-                   delete values.pool;
-               }
-               return values;
+           let mode = me.lookup('modeSelector').getValue();
+           if (mode === 'all') {
+               vmgrid.selModel.selectAll(true);
            }
-       });
-
-       var update_vmid_selection = function(list, mode) {
-           if (mode !== 'all' && mode !== 'pool') {
-               sm.deselectAll(true);
-               if (list) {
-                   Ext.Array.each(list.split(','), function(vmid) {
-                       var rec = store.findRecord('vmid', vmid, 0, false, true, true);
-                       if (rec) {
-                           sm.select(rec, true);
-                       }
-                   });
-               }
+           if (mode === 'pool') {
+               me.selectPoolMembers();
            }
-       };
-
-       vmidField.on('change', function(f, value) {
-           var mode = selModeField.getValue();
-           update_vmid_selection(value, mode);
-       });
+       },
 
-       selModeField.on('change', function(f, value, oldValue) {
-           if (oldValue === 'pool') {
-               store.removeFilter('poolFilter');
+       storageChange: function(f, v) {
+           let me = this;
+           let rec = f.getStore().findRecord('storage', v, 0, false, true, true);
+           let compressionSelector = me.lookup('compressionSelector');
+
+           if (rec?.data?.type === 'pbs') {
+               compressionSelector.setValue('zstd');
+               compressionSelector.setDisabled(true);
+           } else if (!compressionSelector.getEditable()) {
+               compressionSelector.setDisabled(false);
            }
+       },
 
-           if (oldValue === 'all') {
-               sm.deselectAll(true);
-               vmidField.setValue('');
-           }
+       selectPoolMembers: function() {
+           let me = this;
+           let mode = me.lookup('modeSelector').getValue();
 
-           if (value === 'all') {
-               sm.selectAll(true);
-               vmgrid.setDisabled(true);
-           } else {
-               vmgrid.setDisabled(false);
+           if (mode !== 'pool') {
+               return;
            }
 
-           if (value === 'pool') {
-               vmgrid.setDisabled(true);
-               vmidField.setValue('');
-               selPool.setVisible(true);
-               selPool.allowBlank = false;
-               selectPoolMembers(selPool.value);
+           let vmgrid = me.lookup('vmgrid');
+           let poolid = me.lookup('poolSelector').getValue();
 
-           } else {
-               selPool.setVisible(false);
-               selPool.allowBlank = true;
+           vmgrid.getSelectionModel().deselectAll(true);
+           if (!poolid) {
+               return;
            }
-           var list = vmidField.getValue();
-           update_vmid_selection(list, value);
-       });
-
-       var reload = function() {
-           store.load({
-               params: { type: 'vm' },
-               callback: function() {
-                   var node = nodesel.getValue();
-                   store.clearFilter();
-                   store.filterBy(function(rec) {
-                       return (!node || node.length === 0 || rec.get('node') === node);
-                   });
-                   var list = vmidField.getValue();
-                   var mode = selModeField.getValue();
-                   if (mode === 'all') {
-                       sm.selectAll(true);
-                   } else if (mode === 'pool'){
-                       selectPoolMembers(selPool.value);
-                   } else {
-                       update_vmid_selection(list, mode);
-                   }
-               }
-           });
-       };
-
-        Ext.applyIf(me, {
-            subject: gettext("Backup Job"),
-            url: url,
-            method: method,
-           items: [ ipanel, vmgrid ]
-        });
-
-        me.callParent();
-
-        if (me.isCreate) {
-           selModeField.setValue('include');
-       } else {
-            me.load({
-               success: function(response, options) {
-                   var data = response.result.data;
-
-                   data.dow = data.dow.split(',');
-
-                   if (data.all || data.exclude) {
-                       if (data.exclude) {
-                           data.vmid = data.exclude;
-                           data.selMode = 'exclude';
-                       } else {
-                           data.vmid = '';
-                           data.selMode = 'all';
-                       }
-                   } else if (data.pool) {
-                       data.selMode = 'pool';
-                       data.selPool = data.pool;
-                   } else {
-                       data.selMode = 'include';
-                   }
-
-                   me.setValues(data);
-               }
-            });
-        }
-
-       reload();
-    }
-});
-
+           vmgrid.getStore().filter([
+               {
+                   id: 'poolFilter',
+                   property: 'pool',
+                   value: poolid,
+               },
+           ]);
+           vmgrid.selModel.selectAll(true);
+       },
 
-Ext.define('PVE.dc.BackupDiskTree', {
-    extend: 'Ext.tree.Panel',
-    alias: 'widget.pveBackupDiskTree',
+       modeChange: function(f, value, oldValue) {
+           let me = this;
+           let vmgrid = me.lookup('vmgrid');
+           vmgrid.getStore().removeFilter('poolFilter');
 
-    folderSort: true,
-    rootVisible: false,
+           if (oldValue === 'all' && value !== 'all') {
+               vmgrid.getSelectionModel().deselectAll(true);
+           }
 
-    store: {
-       sorters: 'id',
-       data: {},
-    },
+           if (value === 'all') {
+               vmgrid.getSelectionModel().selectAll(true);
+           }
 
-    tools: [
-       {
-           type: 'expand',
-           tooltip: gettext('Expand All'),
-           scope: this,
-           callback: function(panel) {
-               panel.expandAll();
-           },
-       },
-       {
-           type: 'collapse',
-           tooltip: gettext('Collapse All'),
-           scope: this,
-           callback: function(panel) {
-               panel.collapseAll();
+           if (value === 'pool') {
+               me.selectPoolMembers();
            }
        },
-    ],
 
-    columns: [
-       {
-           xtype: 'treecolumn',
-           text: gettext('Guest Image'),
-           renderer: function(value, meta, record) {
-               if (record.data.type) {
-                   // guest level
-                   let ret = value;
-                   if (record.data.name) {
-                       ret += " (" + record.data.name + ")";
-                   }
-                   return ret;
-               } else {
-                   // volume level
-                   // extJS needs unique IDs but we only want to show the
-                   // volumes key from "vmid:key"
-                   return value.split(':')[1] + " - " + record.data.name;
-               }
-           },
-           dataIndex: 'id',
-           flex: 6,
-       },
-       {
-           text: gettext('Type'),
-           dataIndex: 'type',
-           flex: 1,
+       compressionChange: function(f, value, oldValue) {
+           this.getView().lookup('backupAdvanced').updateCompression(value, f.isDisabled());
        },
-       {
-           text: gettext('Backup Job'),
-           renderer: PVE.Utils.render_backup_status,
-           dataIndex: 'included',
-           flex: 3,
-       },
-    ],
 
-    reload: function() {
-       var me = this;
-       var sm = me.getSelectionModel();
-
-       Proxmox.Utils.API2Request({
-           url: "/cluster/backup/" + me.jobid + "/included_volumes",
-           waitMsgTarget: me,
-           method: 'GET',
-           failure: function(response, opts) {
-               Proxmox.Utils.setErrorMask(me, response.htmlStatus);
-           },
-           success: function(response, opts) {
-               sm.deselectAll();
-               me.setRootNode(response.result.data);
-               me.expandAll();
-           },
-       });
-    },
-
-    initComponent: function() {
-       var me = this;
+       compressionDisable: function(f) {
+           this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), true);
+       },
 
-       if (!me.jobid) {
-           throw "no job id specified";
-       }
+       compressionEnable: function(f) {
+           this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), false);
+       },
 
-       var sm = Ext.create('Ext.selection.TreeModel', {});
+       prepareValues: function(data) {
+           let me = this;
+           let viewModel = me.getViewModel();
+
+           // Migrate 'new'-old notification-policy back to old-old mailnotification.
+           // Only should affect users who used pve-manager from pvetest. This was a remnant of
+           // notifications before the  overhaul.
+           let policy = data['notification-policy'];
+           if (policy === 'always' || policy === 'failure') {
+               data.mailnotification = policy;
+           }
 
-       Ext.apply(me, {
-           selModel: sm,
-           fields: ['id', 'type',
-               {
-                   type: 'string',
-                   name: 'iconCls',
-                   calculate: function(data) {
-                       var txt = 'fa x-fa-tree fa-';
-                       if (data.leaf && !data.type) {
-                           return txt + 'hdd-o';
-                       } else if (data.type === 'qemu') {
-                           return txt + 'desktop';
-                       } else if (data.type === 'lxc') {
-                           return txt + 'cube';
-                       } else {
-                           return txt + 'question-circle';
-                       }
-                   }
-               }
-           ],
-           header: {
-               items: [{
-                   xtype: 'textfield',
-                   fieldLabel: gettext('Search'),
-                   labelWidth: 50,
-                   emptyText: 'Name, VMID, Type',
-                   width: 200,
-                   padding: '0 5 0 0',
-                   enableKeyEvents: true,
-                   listeners: {
-                       buffer: 500,
-                       keyup: function(field) {
-                           let searchValue = field.getValue();
-                           searchValue = searchValue.toLowerCase();
-
-                           me.store.clearFilter(true);
-                           me.store.filterBy(function(record) {
-                               let match = false;
-
-                               let data = '';
-                               if (record.data.depth == 0) {
-                                   return true;
-                               } else if (record.data.depth == 1) {
-                                   data = record.data;
-                               } else if (record.data.depth == 2) {
-                                   data = record.parentNode.data;
-                               }
-
-                               Ext.each(['name', 'id', 'type'], function(property) {
-                                   if (data[property] === null) {
-                                       return;
-                                   }
-
-                                   let v = data[property].toString();
-                                   if (v !== undefined) {
-                                       v = v.toLowerCase();
-                                       if (v.includes(searchValue)) {
-                                           match = true;
-                                           return;
-                                       }
-                                   }
-                               });
-                               return match;
-                           });
-                       }
-                   }
+           if (data.exclude) {
+               data.vmid = data.exclude;
+               data.selMode = 'exclude';
+           } else if (data.all) {
+               data.vmid = '';
+               data.selMode = 'all';
+           } else if (data.pool) {
+               data.selMode = 'pool';
+               data.selPool = data.pool;
+           } else {
+               data.selMode = 'include';
+           }
+           viewModel.set('selMode', data.selMode);
+
+           if (data['prune-backups']) {
+               Object.assign(data, data['prune-backups']);
+               delete data['prune-backups'];
+           } else if (data.maxfiles !== undefined) {
+               if (data.maxfiles > 0) {
+                   data['keep-last'] = data.maxfiles;
+               } else {
+                   data['keep-all'] = 1;
                }
-           ]},
-       });
+               delete data.maxfiles;
+           }
 
-       me.callParent();
+           if (data['notes-template']) {
+               data['notes-template'] =
+                   PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
+           }
 
-       me.reload();
-    }
-});
+           if (data.performance) {
+               Object.assign(data, data.performance);
+               delete data.performance;
+           }
 
-Ext.define('PVE.dc.BackupInfo', {
-    extend: 'Proxmox.panel.InputPanel',
-    alias: 'widget.pveBackupInfo',
+           return data;
+       },
 
-    padding: '5 0 5 10',
+       init: function(view) {
+           let me = this;
 
-    column1: [
-       {
-           name: 'node',
-           fieldLabel: gettext('Node'),
-           xtype: 'displayfield',
-           renderer: function (value) {
-               if (!value) {
-                   return '-- ' + gettext('All') + ' --';
-               } else {
-                   return value;
-               }
-           },
-       },
-       {
-           name: 'storage',
-           fieldLabel: gettext('Storage'),
-           xtype: 'displayfield',
-       },
-       {
-           name: 'dow',
-           fieldLabel: gettext('Day of week'),
-           xtype: 'displayfield',
-           renderer: PVE.Utils.render_backup_days_of_week,
-       },
-       {
-           name: 'starttime',
-           fieldLabel: gettext('Start Time'),
-           xtype: 'displayfield',
-       },
-       {
-           name: 'selMode',
-           fieldLabel: gettext('Selection mode'),
-           xtype: 'displayfield',
-       },
-       {
-           name: 'pool',
-           fieldLabel: gettext('Pool to backup'),
-           xtype: 'displayfield',
-       }
-    ],
-    column2: [
-       {
-           name: 'mailto',
-           fieldLabel: gettext('Send email to'),
-           xtype: 'displayfield',
-       },
-       {
-           name: 'mailnotification',
-           fieldLabel: gettext('Email notification'),
-           xtype: 'displayfield',
-           renderer: function (value) {
-               let msg;
-               switch (value) {
-                   case 'always':
-                       msg = gettext('Always');
-                       break;
-                   case 'failure':
-                       msg = gettext('On failure only');
-                       break;
-               }
-               return msg;
-           },
-       },
-       {
-           name: 'compress',
-           fieldLabel: gettext('Compression'),
-           xtype: 'displayfield',
+           if (view.isCreate) {
+               me.lookup('modeSelector').setValue('include');
+           } else {
+               view.load({
+                   success: function(response, _options) {
+                       let values = me.prepareValues(response.result.data);
+                       view.setValues(values);
+                   },
+               });
+           }
        },
-       {
-           name: 'mode',
-           fieldLabel: gettext('Mode'),
-           xtype: 'displayfield',
-           renderer: function (value) {
-               let msg;
-               switch (value) {
-                   case 'snapshot':
-                       msg = gettext('Snapshot');
-                       break;
-                   case 'suspend':
-                       msg = gettext('Suspend');
-                       break;
-                   case 'stop':
-                       msg = gettext('Stop');
-                       break;
-               }
-               return msg;
-           },
+    },
+
+    viewModel: {
+       data: {
+           selMode: 'include',
+           notificationMode: '__default__',
+           mailto: '',
+           mailNotification: 'always',
        },
-       {
-           name: 'enabled',
-           fieldLabel: gettext('Enabled'),
-           xtype: 'displayfield',
-           renderer: function (value) {
-               if (PVE.Parser.parseBoolean(value.toString())) {
-                   return gettext('Yes');
-               } else {
-                   return gettext('No');
-               }
+
+       formulas: {
+           poolMode: (get) => get('selMode') === 'pool',
+           disableVMSelection: (get) => get('selMode') !== 'include' &&
+               get('selMode') !== 'exclude',
+           showMailtoFields: (get) =>
+               ['auto', 'legacy-sendmail', '__default__'].includes(get('notificationMode')),
+           enableMailnotificationField: (get) => {
+               let mode = get('notificationMode');
+               let mailto = get('mailto');
+
+               return (['auto', '__default__'].includes(mode) && mailto) ||
+                   mode === 'legacy-sendmail';
            },
        },
-    ],
-
-    setValues: function(values) {
-       var me = this;
-
-        Ext.iterate(values, function(fieldId, val) {
-           let field = me.query('[isFormField][name=' + fieldId + ']')[0];
-           if (field) {
-               field.setValue(val);
-            }
-       });
-
-       // selection Mode depends on the presence/absence of several keys
-       let selModeField = me.query('[isFormField][name=selMode]')[0];
-       let selMode = 'none';
-       if (values.vmid) {
-           selMode = gettext('Include selected VMs');
-       }
-       if (values.all) {
-           selMode = gettext('All');
-       }
-       if (values.exclude) {
-            selMode = gettext('Exclude selected VMs');
-       }
-       if (values.pool) {
-           selMode = gettext('Pool based');
-       }
-       selModeField.setValue(selMode);
-
-       if (!values.pool) {
-           let poolField = me.query('[isFormField][name=pool]')[0];
-           poolField.setVisible(0);
-       }
     },
 
-    initComponent: function() {
-       var me = this;
-
-       if (!me.record) {
-           throw "no data provided";
-       }
-       me.callParent();
-
-       me.setValues(me.record);
-    }
-});
-
-
-Ext.define('PVE.dc.BackedGuests', {
-    extend: 'Ext.grid.GridPanel',
-    alias: 'widget.pveBackedGuests',
-
-    textfilter: '',
-
-    columns: [
-       {
-           header: gettext('Type'),
-           dataIndex: "type",
-           renderer: PVE.Utils.render_resource_type,
-           flex: 1,
-           sortable: true,
-       },
-       {
-           header: gettext('VMID'),
-           dataIndex: 'vmid',
-           flex: 1,
-           sortable: true,
-       },
+    items: [
        {
-           header: gettext('Name'),
-           dataIndex: 'name',
-           flex: 2,
-           sortable: true,
-       },
-    ],
-
-    initComponent: function() {
-       let me = this;
-
-       me.store.clearFilter(true);
-
-       Ext.apply(me, {
-           stateful: true,
-           stateId: 'grid-dc-backed-guests',
-           tbar: [
-               '->',
-               gettext('Search') + ':', ' ',
+           xtype: 'tabpanel',
+           region: 'center',
+           layout: 'fit',
+           bodyPadding: 10,
+           items: [
                {
-                   xtype: 'textfield',
-                   width: 200,
-                   emptyText: 'Name, VMID, Type',
-                   enableKeyEvents: true,
-                   listeners: {
-                       buffer: 500,
-                       keyup: function(field) {
-                           let searchValue = field.getValue();
-                           searchValue = searchValue.toLowerCase();
-
-                           me.store.clearFilter(true);
-                           me.store.filterBy(function(record) {
-                               let match = false;
-
-                               Ext.each(['name', 'vmid', 'type'], function(property) {
-                                   if (record.data[property] == null) {
-                                       return;
-                                   }
-
-                                   let v = record.data[property].toString();
-                                   if (v !== undefined) {
-                                       v = v.toLowerCase();
-                                       if (v.includes(searchValue)) {
-                                           match = true;
-                                           return;
-                                       }
-                                   }
-                               });
-                               return match;
-                           });
+                   xtype: 'container',
+                   title: gettext('General'),
+                   region: 'center',
+                   layout: {
+                       type: 'vbox',
+                       align: 'stretch',
+                   },
+                   items: [
+                       {
+                           xtype: 'inputpanel',
+                           onlineHelp: 'chapter_vzdump',
+                           column1: [
+                               {
+                                   xtype: 'pveNodeSelector',
+                                   name: 'node',
+                                   fieldLabel: gettext('Node'),
+                                   allowBlank: true,
+                                   editable: true,
+                                   autoSelect: false,
+                                   emptyText: '-- ' + gettext('All') + ' --',
+                                   listeners: {
+                                       change: 'nodeChange',
+                                   },
+                               },
+                               {
+                                   xtype: 'pveStorageSelector',
+                                   reference: 'storageSelector',
+                                   fieldLabel: gettext('Storage'),
+                                   clusterView: true,
+                                   storageContent: 'backup',
+                                   allowBlank: false,
+                                   name: 'storage',
+                                   listeners: {
+                                       change: 'storageChange',
+                                   },
+                               },
+                               {
+                                   xtype: 'pveCalendarEvent',
+                                   fieldLabel: gettext('Schedule'),
+                                   allowBlank: false,
+                                   name: 'schedule',
+                               },
+                               {
+                                   xtype: 'proxmoxKVComboBox',
+                                   reference: 'modeSelector',
+                                   comboItems: [
+                                       ['include', gettext('Include selected VMs')],
+                                       ['all', gettext('All')],
+                                       ['exclude', gettext('Exclude selected VMs')],
+                                       ['pool', gettext('Pool based')],
+                                   ],
+                                   fieldLabel: gettext('Selection mode'),
+                                   name: 'selMode',
+                                   value: '',
+                                   bind: {
+                                       value: '{selMode}',
+                                   },
+                                   listeners: {
+                                       change: 'modeChange',
+                                   },
+                               },
+                               {
+                                   xtype: 'pvePoolSelector',
+                                   reference: 'poolSelector',
+                                   fieldLabel: gettext('Pool to backup'),
+                                   hidden: true,
+                                   allowBlank: false,
+                                   name: 'pool',
+                                   listeners: {
+                                       change: 'selectPoolMembers',
+                                   },
+                                   bind: {
+                                       hidden: '{!poolMode}',
+                                       disabled: '{!poolMode}',
+                                   },
+                               },
+                           ],
+                           column2: [
+                               {
+                                   xtype: 'proxmoxKVComboBox',
+                                   comboItems: [
+                                       [
+                                           '__default__',
+                                           Ext.String.format(
+                                               gettext('{0} (Auto)'), Proxmox.Utils.defaultText,
+                                           ),
+                                       ],
+                                       ['auto', gettext('Auto')],
+                                       ['legacy-sendmail', gettext('Email (legacy)')],
+                                       ['notification-system', gettext('Notification system')],
+                                   ],
+                                   fieldLabel: gettext('Notification mode'),
+                                   name: 'notification-mode',
+                                   value: '__default__',
+                                   cbind: {
+                                       deleteEmpty: '{!isCreate}',
+                                   },
+                                   bind: {
+                                       value: '{notificationMode}',
+                                   },
+                               },
+                               {
+                                   xtype: 'textfield',
+                                   fieldLabel: gettext('Send email to'),
+                                   name: 'mailto',
+                                   bind: {
+                                       hidden: '{!showMailtoFields}',
+                                       value: '{mailto}',
+                                   },
+                               },
+                               {
+                                   xtype: 'pveEmailNotificationSelector',
+                                   fieldLabel: gettext('Send email'),
+                                   name: 'mailnotification',
+                                   cbind: {
+                                       value: (get) => get('isCreate') ? 'always' : '',
+                                       deleteEmpty: '{!isCreate}',
+                                   },
+                                   bind: {
+                                       hidden: '{!showMailtoFields}',
+                                       disabled: '{!enableMailnotificationField}',
+                                       value: '{mailNotification}',
+                                   },
+                               },
+                               {
+                                   xtype: 'pveBackupCompressionSelector',
+                                   reference: 'compressionSelector',
+                                   fieldLabel: gettext('Compression'),
+                                   name: 'compress',
+                                   cbind: {
+                                       deleteEmpty: '{!isCreate}',
+                                   },
+                                   value: 'zstd',
+                                   listeners: {
+                                       change: 'compressionChange',
+                                       disable: 'compressionDisable',
+                                       enable: 'compressionEnable',
+                                   },
+                               },
+                               {
+                                   xtype: 'pveBackupModeSelector',
+                                   fieldLabel: gettext('Mode'),
+                                   value: 'snapshot',
+                                   name: 'mode',
+                               },
+                               {
+                                   xtype: 'proxmoxcheckbox',
+                                   fieldLabel: gettext('Enable'),
+                                   name: 'enabled',
+                                   uncheckedValue: 0,
+                                   defaultValue: 1,
+                                   checked: true,
+                               },
+                           ],
+                           columnB: [
+                               {
+                                   xtype: 'proxmoxtextfield',
+                                   name: 'comment',
+                                   fieldLabel: gettext('Job Comment'),
+                                   cbind: {
+                                       deleteEmpty: '{!isCreate}',
+                                   },
+                                   autoEl: {
+                                       tag: 'div',
+                                       'data-qtip': gettext('Description of the job'),
+                                   },
+                               },
+                               {
+                                   xtype: 'vmselector',
+                                   reference: 'vmgrid',
+                                   height: 300,
+                                   name: 'vmid',
+                                   disabled: true,
+                                   allowBlank: false,
+                                   columnSelection: ['vmid', 'node', 'status', 'name', 'type'],
+                                   bind: {
+                                       disabled: '{disableVMSelection}',
+                                   },
+                               },
+                           ],
+                           onGetValues: function(values) {
+                               return this.up('window').getController().onGetValues(values);
+                           },
+                       },
+                   ],
+               },
+               {
+                   xtype: 'pveBackupJobPrunePanel',
+                   title: gettext('Retention'),
+                   cbind: {
+                       isCreate: '{isCreate}',
+                   },
+                   keepAllDefaultForCreate: false,
+                   showPBSHint: false,
+                   fallbackHintHtml: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
+               },
+               {
+                   xtype: 'inputpanel',
+                   title: gettext('Note Template'),
+                   region: 'center',
+                   layout: {
+                       type: 'vbox',
+                       align: 'stretch',
+                   },
+                   onGetValues: function(values) {
+                       if (values['notes-template']) {
+                           values['notes-template'] =
+                               PVE.Utils.escapeNotesTemplate(values['notes-template']);
                        }
-                   }
-               }
+                       return values;
+                   },
+                   items: [
+                       {
+                           xtype: 'textarea',
+                           name: 'notes-template',
+                           fieldLabel: gettext('Backup Notes'),
+                           height: 100,
+                           maxLength: 512,
+                           cbind: {
+                               deleteEmpty: '{!isCreate}',
+                               value: (get) => get('isCreate') ? '{{guestname}}' : undefined,
+                           },
+                       },
+                       {
+                           xtype: 'box',
+                           style: {
+                               margin: '8px 0px',
+                               'line-height': '1.5em',
+                           },
+                           html: gettext('The notes are added to each backup created by this job.')
+                               + '<br>'
+                               + Ext.String.format(
+                                   gettext('Possible template variables are: {0}'),
+                                   PVE.Utils.notesTemplateVars.map(v => `<code>{{${v}}}</code>`).join(', '),
+                               ),
+                       },
+                   ],
+               },
+               {
+                   xtype: 'pveBackupAdvancedOptionsPanel',
+                   reference: 'backupAdvanced',
+                   title: gettext('Advanced'),
+                   cbind: {
+                       isCreate: '{isCreate}',
+                   },
+               },
            ],
-           viewConfig: {
-               stripeRows: true,
-               trackOver: false,
-            },
-       });
-       me.callParent();
-    },
+       },
+    ],
 });
 
 Ext.define('PVE.dc.BackupView', {
@@ -805,95 +517,90 @@ Ext.define('PVE.dc.BackupView', {
 
     allText: '-- ' + gettext('All') + ' --',
 
-    initComponent : function() {
-       var me = this;
+    initComponent: function() {
+       let me = this;
 
-       var store = new Ext.data.Store({
+       let store = new Ext.data.Store({
            model: 'pve-cluster-backup',
            proxy: {
-                type: 'proxmox',
-               url: "/api2/json/cluster/backup"
-           }
+               type: 'proxmox',
+               url: "/api2/json/cluster/backup",
+           },
        });
 
-       var not_backed_store = new Ext.data.Store({
+       let not_backed_store = new Ext.data.Store({
            sorters: 'vmid',
-           proxy:{
+           proxy: {
                type: 'proxmox',
-               url: 'api2/json/cluster/backupinfo/not_backed_up',
+               url: 'api2/json/cluster/backup-info/not-backed-up',
            },
        });
 
-       var reload = function() {
+       let noBackupJobInfoButton;
+       let reload = function() {
            store.load();
            not_backed_store.load({
-               callback: function(records, operation, success) {
-                   if (records.length) {
-                       not_backed_warning.setVisible(true);
-                       not_backed_btn.setVisible(true);
-                   } else {
-                       not_backed_warning.setVisible(false);
-                       not_backed_btn.setVisible(false);
-                   }
-               },
+               callback: records => noBackupJobInfoButton.setVisible(records.length > 0),
            });
        };
 
-       var sm = Ext.create('Ext.selection.RowModel', {});
+       let sm = Ext.create('Ext.selection.RowModel', {});
 
-       var run_editor = function() {
-           var rec = sm.getSelection()[0];
+       let run_editor = function() {
+           let rec = sm.getSelection()[0];
            if (!rec) {
                return;
            }
 
-           var win = Ext.create('PVE.dc.BackupEdit', {
-               jobid: rec.data.id
+           Ext.create('PVE.dc.BackupEdit', {
+               autoShow: true,
+               jobid: rec.data.id,
+               listeners: {
+                   destroy: () => reload(),
+               },
            });
-           win.on('destroy', reload);
-           win.show();
        };
 
-       var run_detail = function() {
-           let me = this;
-           let record = sm.getSelection()[0]
+       let run_detail = function() {
+           let record = sm.getSelection()[0];
            if (!record) {
                return;
            }
-           let infoview = Ext.create('PVE.dc.BackupInfo', {
-               flex: 0,
-               layout: 'fit',
-               record: record.data,
-           });
-           let disktree = Ext.create('PVE.dc.BackupDiskTree', {
-               title: gettext('Included disks'),
-               flex: 1,
-               jobid: record.data.id,
-           });
-
            Ext.create('Ext.window.Window', {
                modal: true,
                width: 800,
-               height: 600,
-               stateful: true,
-               stateId: 'backup-detail-view',
+               height: Ext.getBody().getViewSize().height > 1000 ? 800 : 600, // factor out as common infra?
                resizable: true,
                layout: 'fit',
                title: gettext('Backup Details'),
-
-               items:[{
-                   xtype: 'panel',
-                   region: 'center',
-                   layout: {
-                       type: 'vbox',
-                       align: 'stretch'
+               items: [
+                   {
+                       xtype: 'panel',
+                       region: 'center',
+                       layout: {
+                           type: 'vbox',
+                           align: 'stretch',
+                       },
+                       items: [
+                           {
+                               xtype: 'pveBackupInfo',
+                               flex: 0,
+                               layout: 'fit',
+                               record: record.data,
+                           },
+                           {
+                               xtype: 'pveBackupDiskTree',
+                               title: gettext('Included disks'),
+                               flex: 1,
+                               jobid: record.data.id,
+                           },
+                       ],
                    },
-                   items: [infoview, disktree],
-               }]
+               ],
            }).show();
        };
 
-       var run_backup_now = function(job) {
+       let run_backup_now = function(job) {
            job = Ext.clone(job);
 
            let jobNode = job.node;
@@ -902,9 +609,20 @@ Ext.define('PVE.dc.BackupView', {
            delete job.starttime;
            delete job.dow;
            delete job.id;
+           delete job.schedule;
+           delete job.type;
            delete job.node;
+           delete job.comment;
+           delete job['next-run'];
+           delete job['repeat-missed'];
            job.all = job.all === true ? 1 : 0;
 
+           ['performance', 'prune-backups', 'fleecing'].forEach(key => {
+               if (job[key]) {
+                   job[key] = PVE.Parser.printPropertyString(job[key]);
+               }
+           });
+
            let allNodes = PVE.data.ResourceStore.getNodes();
            let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
            let errors = [];
@@ -914,11 +632,10 @@ Ext.define('PVE.dc.BackupView', {
                    Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
                    return;
                }
-               nodes = [ jobNode ];
+               nodes = [jobNode];
            } else {
                let unkownNodes = allNodes.filter(node => node.status !== 'online');
-               if (unkownNodes.length > 0)
-                   errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));
+               if (unkownNodes.length > 0) {errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));}
            }
            let jobTotalCount = nodes.length, jobsStarted = 0;
 
@@ -929,11 +646,11 @@ Ext.define('PVE.dc.BackupView', {
                progressText: '0/' + jobTotalCount,
            });
 
-           let postRequest = function () {
+           let postRequest = function() {
                jobsStarted++;
                Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
 
-               if (jobsStarted == jobTotalCount) {
+               if (jobsStarted === jobTotalCount) {
                    Ext.Msg.hide();
                    if (errors.length > 0) {
                        Ext.Msg.alert('Error', 'Some errors have been encountered:<br />' + errors.join('<br />'));
@@ -945,47 +662,19 @@ Ext.define('PVE.dc.BackupView', {
                url: '/nodes/' + node + '/vzdump',
                method: 'POST',
                params: job,
-               failure: function (response, opts) {
+               failure: function(response, opts) {
                    errors.push(node + ': ' + response.htmlStatus);
                    postRequest();
                },
-               success: postRequest
+               success: postRequest,
            }));
        };
 
-       var run_show_not_backed = function() {
-           var me = this;
-           var backedinfo = Ext.create('PVE.dc.BackedGuests', {
-               flex: 1,
-               layout: 'fit',
-               store: not_backed_store,
-           });
-
-           var win = Ext.create('Ext.window.Window', {
-               modal: true,
-               width: 600,
-               height: 500,
-               resizable: true,
-               layout: 'fit',
-               title: gettext('Guests without backup job'),
-
-               items:[{
-                   xtype: 'panel',
-                   region: 'center',
-                   layout: {
-                       type: 'vbox',
-                       align: 'stretch'
-                   },
-                   items: [backedinfo],
-               }]
-           }).show();
-       };
-
        var edit_btn = new Proxmox.button.Button({
            text: gettext('Edit'),
            disabled: true,
            selModel: sm,
-           handler: run_editor
+           handler: run_editor,
        });
 
        var run_btn = new Proxmox.button.Button({
@@ -1008,9 +697,9 @@ Ext.define('PVE.dc.BackupView', {
                            return;
                        }
                        run_backup_now(rec.data);
-                   }
+                   },
                });
-           }
+           },
        });
 
        var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
@@ -1018,7 +707,7 @@ Ext.define('PVE.dc.BackupView', {
            baseurl: '/cluster/backup',
            callback: function() {
                reload();
-           }
+           },
        });
 
        var detail_btn = new Proxmox.button.Button({
@@ -1029,15 +718,40 @@ Ext.define('PVE.dc.BackupView', {
            handler: run_detail,
        });
 
-       var not_backed_warning = Ext.create('Ext.toolbar.TextItem', {
-           html: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
+       noBackupJobInfoButton = new Proxmox.button.Button({
+           text: `${gettext('Show')}: ${gettext('Guests Without Backup Job')}`,
+           tooltip: gettext('Some guests are not covered by any backup job.'),
+           iconCls: 'fa fa-fw fa-exclamation-circle',
            hidden: true,
-       });
-
-       var not_backed_btn = new Proxmox.button.Button({
-           text: gettext('Show'),
-           hidden: true,
-           handler: run_show_not_backed,
+           handler: () => {
+               Ext.create('Ext.window.Window', {
+                   autoShow: true,
+                   modal: true,
+                   width: 600,
+                   height: 500,
+                   resizable: true,
+                   layout: 'fit',
+                   title: gettext('Guests Without Backup Job'),
+                   items: [
+                       {
+                           xtype: 'panel',
+                           region: 'center',
+                           layout: {
+                               type: 'vbox',
+                               align: 'stretch',
+                           },
+                           items: [
+                               {
+                                   xtype: 'pveBackedGuests',
+                                   flex: 1,
+                                   layout: 'fit',
+                                   store: not_backed_store,
+                               },
+                           ],
+                       },
+                   ],
+               });
+           },
        });
 
        Proxmox.Utils.monStoreErrors(me, store);
@@ -1048,37 +762,61 @@ Ext.define('PVE.dc.BackupView', {
            stateful: true,
            stateId: 'grid-dc-backup',
            viewConfig: {
-               trackOver: false
+               trackOver: false,
            },
-           tbar: [
-               {
-                   text: gettext('Add'),
-                   handler: function() {
-                       var win = Ext.create('PVE.dc.BackupEdit',{});
-                       win.on('destroy', reload);
-                       win.show();
-                   }
-               },
-               '-',
-               remove_btn,
-               edit_btn,
-               detail_btn,
-               '-',
-               run_btn,
-               '->',
-               not_backed_warning,
-               not_backed_btn,
-           ],
+           dockedItems: [{
+               xtype: 'toolbar',
+               overflowHandler: 'scroller',
+               dock: 'top',
+               items: [
+                   {
+                       text: gettext('Add'),
+                       handler: function() {
+                           var win = Ext.create('PVE.dc.BackupEdit', {});
+                           win.on('destroy', reload);
+                           win.show();
+                       },
+                   },
+                   '-',
+                   remove_btn,
+                   edit_btn,
+                   detail_btn,
+                   '-',
+                   run_btn,
+                   '->',
+                   noBackupJobInfoButton,
+                   '-',
+                   {
+                       xtype: 'proxmoxButton',
+                       selModel: null,
+                       text: gettext('Schedule Simulator'),
+                       handler: () => {
+                           let record = sm.getSelection()[0];
+                           let schedule;
+                           if (record) {
+                               schedule = record.data.schedule;
+                           }
+                           Ext.create('PVE.window.ScheduleSimulator', {
+                               autoShow: true,
+                               schedule,
+                           });
+                       },
+                   },
+               ],
+           }],
            columns: [
                {
                    header: gettext('Enabled'),
                    width: 80,
                    dataIndex: 'enabled',
-                   xtype: 'checkcolumn',
+                   align: 'center',
+                   renderer: Proxmox.Utils.renderEnabledIcon,
                    sortable: true,
-                   disabled: true,
-                   disabledCls: 'x-item-enabled',
-                   stopSelection: false
+               },
+               {
+                   header: gettext('ID'),
+                   dataIndex: 'id',
+                   hidden: true,
                },
                {
                    header: gettext('Node'),
@@ -1090,53 +828,72 @@ Ext.define('PVE.dc.BackupView', {
                            return value;
                        }
                        return me.allText;
-                   }
+                   },
                },
                {
-                   header: gettext('Day of week'),
-                   width: 200,
-                   sortable: false,
-                   dataIndex: 'dow',
-                   renderer: PVE.Utils.render_backup_days_of_week
+                   header: gettext('Schedule'),
+                   width: 150,
+                   dataIndex: 'schedule',
                },
                {
-                   header: gettext('Start Time'),
-                   width: 60,
-                   sortable: true,
-                   dataIndex: 'starttime'
+                   text: gettext('Next Run'),
+                   dataIndex: 'next-run',
+                   width: 150,
+                   renderer: PVE.Utils.render_next_event,
                },
                {
                    header: gettext('Storage'),
                    width: 100,
                    sortable: true,
-                   dataIndex: 'storage'
+                   dataIndex: 'storage',
                },
                {
-                   header: gettext('Selection'),
+                   header: gettext('Comment'),
+                   dataIndex: 'comment',
+                   renderer: Ext.htmlEncode,
+                   sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''),
                    flex: 1,
+               },
+               {
+                   header: gettext('Retention'),
+                   dataIndex: 'prune-backups',
+                   renderer: v => v ? PVE.Parser.printPropertyString(v) : gettext('Fallback from storage config'),
+                   flex: 2,
+               },
+               {
+                   header: gettext('Selection'),
+                   flex: 4,
                    sortable: false,
                    dataIndex: 'vmid',
-                   renderer: PVE.Utils.render_backup_selection
-               }
+                   renderer: PVE.Utils.render_backup_selection,
+               },
            ],
            listeners: {
                activate: reload,
-               itemdblclick: run_editor
-           }
+               itemdblclick: run_editor,
+           },
        });
 
        me.callParent();
-    }
+    },
 }, function() {
-
     Ext.define('pve-cluster-backup', {
        extend: 'Ext.data.Model',
        fields: [
-           'id', 'starttime', 'dow',
-           'storage', 'node', 'vmid', 'exclude',
-           'mailto', 'pool', 'compress', 'mode',
+           'id',
+           'compress',
+           'dow',
+           'exclude',
+           'mailto',
+           'mode',
+           'node',
+           'pool',
+           'prune-backups',
+           'starttime',
+           'storage',
+           'vmid',
            { name: 'enabled', type: 'boolean' },
-           { name: 'all', type: 'boolean' }
-       ]
+           { name: 'all', type: 'boolean' },
+       ],
     });
 });