mirror of
https://git.proxmox.com/git/pve-manager
synced 2025-08-26 03:53:08 +00:00

'selectPoolMembers' will be called when the poolid field changes. (That can even happen when the mode is not even 'pool') Due to how the fields are set, there is a race condition that this will be called after the remaining fields were set up, including the VM list that might have entries selected. Since the first thing we do here is to deselect all, this wiped the virtual guest selection sometimes. To fix it, check if we're actually in the correct mode before doing anything. Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
813 lines
18 KiB
JavaScript
813 lines
18 KiB
JavaScript
Ext.define('PVE.dc.BackupEdit', {
|
|
extend: 'Proxmox.window.Edit',
|
|
alias: ['widget.pveDcBackupEdit'],
|
|
|
|
mixins: ['Proxmox.Mixin.CBind'],
|
|
|
|
defaultFocus: undefined,
|
|
|
|
subject: gettext("Backup Job"),
|
|
bodyPadding: 0,
|
|
|
|
url: '/api2/extjs/cluster/backup',
|
|
method: 'POST',
|
|
isCreate: true,
|
|
|
|
cbindData: function() {
|
|
let me = this;
|
|
if (me.jobid) {
|
|
me.isCreate = false;
|
|
me.method = 'PUT';
|
|
me.url += `/${me.jobid}`;
|
|
}
|
|
return {};
|
|
},
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
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;
|
|
}
|
|
|
|
if (!values.id && isCreate) {
|
|
values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (selMode !== 'pool') {
|
|
delete values.pool;
|
|
}
|
|
return values;
|
|
},
|
|
|
|
nodeChange: function(f, value) {
|
|
let me = this;
|
|
me.lookup('storageSelector').setNodename(value);
|
|
let vmgrid = me.lookup('vmgrid');
|
|
let store = vmgrid.getStore();
|
|
|
|
store.clearFilter();
|
|
store.filterBy(function(rec) {
|
|
return !value || rec.get('node') === value;
|
|
});
|
|
|
|
let mode = me.lookup('modeSelector').getValue();
|
|
if (mode === 'all') {
|
|
vmgrid.selModel.selectAll(true);
|
|
}
|
|
if (mode === 'pool') {
|
|
me.selectPoolMembers();
|
|
}
|
|
},
|
|
|
|
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);
|
|
}
|
|
},
|
|
|
|
selectPoolMembers: function() {
|
|
let me = this;
|
|
let mode = me.lookup('modeSelector').getValue();
|
|
|
|
if (mode !== 'pool') {
|
|
return;
|
|
}
|
|
|
|
let vmgrid = me.lookup('vmgrid');
|
|
let poolid = me.lookup('poolSelector').getValue();
|
|
|
|
vmgrid.getSelectionModel().deselectAll(true);
|
|
if (!poolid) {
|
|
return;
|
|
}
|
|
vmgrid.getStore().filter([
|
|
{
|
|
id: 'poolFilter',
|
|
property: 'pool',
|
|
value: poolid,
|
|
},
|
|
]);
|
|
vmgrid.selModel.selectAll(true);
|
|
},
|
|
|
|
modeChange: function(f, value, oldValue) {
|
|
let me = this;
|
|
let vmgrid = me.lookup('vmgrid');
|
|
vmgrid.getStore().removeFilter('poolFilter');
|
|
|
|
if (oldValue === 'all' && value !== 'all') {
|
|
vmgrid.getSelectionModel().deselectAll(true);
|
|
}
|
|
|
|
if (value === 'all') {
|
|
vmgrid.getSelectionModel().selectAll(true);
|
|
}
|
|
|
|
if (value === 'pool') {
|
|
me.selectPoolMembers();
|
|
}
|
|
},
|
|
|
|
init: function(view) {
|
|
let me = this;
|
|
if (view.isCreate) {
|
|
me.lookup('modeSelector').setValue('include');
|
|
} else {
|
|
view.load({
|
|
success: function(response, _options) {
|
|
let data = response.result.data;
|
|
|
|
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';
|
|
}
|
|
|
|
me.getViewModel().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;
|
|
}
|
|
|
|
if (data['notes-template']) {
|
|
data['notes-template'] =
|
|
PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
|
|
}
|
|
|
|
view.setValues(data);
|
|
},
|
|
});
|
|
}
|
|
},
|
|
},
|
|
|
|
viewModel: {
|
|
data: {
|
|
selMode: 'include',
|
|
},
|
|
|
|
formulas: {
|
|
poolMode: (get) => get('selMode') === 'pool',
|
|
disableVMSelection: (get) => get('selMode') !== 'include' && get('selMode') !== 'exclude',
|
|
},
|
|
},
|
|
|
|
items: [
|
|
{
|
|
xtype: 'tabpanel',
|
|
region: 'center',
|
|
layout: 'fit',
|
|
bodyPadding: 10,
|
|
items: [
|
|
{
|
|
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: 'textfield',
|
|
fieldLabel: gettext('Send email to'),
|
|
name: 'mailto',
|
|
},
|
|
{
|
|
xtype: 'pveEmailNotificationSelector',
|
|
fieldLabel: gettext('Email'),
|
|
name: 'mailnotification',
|
|
cbind: {
|
|
value: (get) => get('isCreate') ? 'always' : '',
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
},
|
|
{
|
|
xtype: 'pveCompressionSelector',
|
|
reference: 'compressionSelector',
|
|
fieldLabel: gettext('Compression'),
|
|
name: 'compress',
|
|
cbind: {
|
|
deleteEmpty: '{!isCreate}',
|
|
},
|
|
value: 'zstd',
|
|
},
|
|
{
|
|
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}',
|
|
},
|
|
},
|
|
],
|
|
advancedColumn1: [
|
|
{
|
|
xtype: 'proxmoxcheckbox',
|
|
fieldLabel: gettext('Repeat missed'),
|
|
name: 'repeat-missed',
|
|
uncheckedValue: 0,
|
|
defaultValue: 0,
|
|
cbind: {
|
|
deleteDefaultValue: '{!isCreate}',
|
|
},
|
|
},
|
|
],
|
|
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(', '),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
Ext.define('PVE.dc.BackupView', {
|
|
extend: 'Ext.grid.GridPanel',
|
|
|
|
alias: ['widget.pveDcBackupView'],
|
|
|
|
onlineHelp: 'chapter_vzdump',
|
|
|
|
allText: '-- ' + gettext('All') + ' --',
|
|
|
|
initComponent: function() {
|
|
let me = this;
|
|
|
|
let store = new Ext.data.Store({
|
|
model: 'pve-cluster-backup',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: "/api2/json/cluster/backup",
|
|
},
|
|
});
|
|
|
|
let not_backed_store = new Ext.data.Store({
|
|
sorters: 'vmid',
|
|
proxy: {
|
|
type: 'proxmox',
|
|
url: 'api2/json/cluster/backup-info/not-backed-up',
|
|
},
|
|
});
|
|
|
|
let noBackupJobInfoButton;
|
|
let reload = function() {
|
|
store.load();
|
|
not_backed_store.load({
|
|
callback: records => noBackupJobInfoButton.setVisible(records.length > 0),
|
|
});
|
|
};
|
|
|
|
let sm = Ext.create('Ext.selection.RowModel', {});
|
|
|
|
let run_editor = function() {
|
|
let rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
let win = Ext.create('PVE.dc.BackupEdit', {
|
|
jobid: rec.data.id,
|
|
});
|
|
win.on('destroy', reload);
|
|
win.show();
|
|
};
|
|
|
|
let run_detail = function() {
|
|
let record = sm.getSelection()[0];
|
|
if (!record) {
|
|
return;
|
|
}
|
|
Ext.create('Ext.window.Window', {
|
|
modal: true,
|
|
width: 800,
|
|
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: 'pveBackupInfo',
|
|
flex: 0,
|
|
layout: 'fit',
|
|
record: record.data,
|
|
},
|
|
{
|
|
xtype: 'pveBackupDiskTree',
|
|
title: gettext('Included disks'),
|
|
flex: 1,
|
|
jobid: record.data.id,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}).show();
|
|
};
|
|
|
|
let run_backup_now = function(job) {
|
|
job = Ext.clone(job);
|
|
|
|
let jobNode = job.node;
|
|
// Remove properties related to scheduling
|
|
delete job.enabled;
|
|
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'].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 = [];
|
|
|
|
if (jobNode !== undefined) {
|
|
if (!nodes.includes(jobNode)) {
|
|
Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
|
|
return;
|
|
}
|
|
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")));}
|
|
}
|
|
let jobTotalCount = nodes.length, jobsStarted = 0;
|
|
|
|
Ext.Msg.show({
|
|
title: gettext('Please wait...'),
|
|
closable: false,
|
|
progress: true,
|
|
progressText: '0/' + jobTotalCount,
|
|
});
|
|
|
|
let postRequest = function() {
|
|
jobsStarted++;
|
|
Ext.Msg.updateProgress(jobsStarted / jobTotalCount, 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 />'));
|
|
}
|
|
}
|
|
};
|
|
|
|
nodes.forEach(node => Proxmox.Utils.API2Request({
|
|
url: '/nodes/' + node + '/vzdump',
|
|
method: 'POST',
|
|
params: job,
|
|
failure: function(response, opts) {
|
|
errors.push(node + ': ' + response.htmlStatus);
|
|
postRequest();
|
|
},
|
|
success: postRequest,
|
|
}));
|
|
};
|
|
|
|
var edit_btn = new Proxmox.button.Button({
|
|
text: gettext('Edit'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: run_editor,
|
|
});
|
|
|
|
var run_btn = new Proxmox.button.Button({
|
|
text: gettext('Run now'),
|
|
disabled: true,
|
|
selModel: sm,
|
|
handler: function() {
|
|
var rec = sm.getSelection()[0];
|
|
if (!rec) {
|
|
return;
|
|
}
|
|
|
|
Ext.Msg.show({
|
|
title: gettext('Confirm'),
|
|
icon: Ext.Msg.QUESTION,
|
|
msg: gettext('Start the selected backup job now?'),
|
|
buttons: Ext.Msg.YESNO,
|
|
callback: function(btn) {
|
|
if (btn !== 'yes') {
|
|
return;
|
|
}
|
|
run_backup_now(rec.data);
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
|
|
selModel: sm,
|
|
baseurl: '/cluster/backup',
|
|
callback: function() {
|
|
reload();
|
|
},
|
|
});
|
|
|
|
var detail_btn = new Proxmox.button.Button({
|
|
text: gettext('Job Detail'),
|
|
disabled: true,
|
|
tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'),
|
|
selModel: sm,
|
|
handler: run_detail,
|
|
});
|
|
|
|
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,
|
|
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);
|
|
|
|
Ext.apply(me, {
|
|
store: store,
|
|
selModel: sm,
|
|
stateful: true,
|
|
stateId: 'grid-dc-backup',
|
|
viewConfig: {
|
|
trackOver: false,
|
|
},
|
|
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',
|
|
align: 'center',
|
|
// TODO: switch to Proxmox.Utils.renderEnabledIcon once available
|
|
renderer: enabled => `<i class="fa fa-${enabled ? 'check' : 'minus'}"></i>`,
|
|
sortable: true,
|
|
},
|
|
{
|
|
header: gettext('ID'),
|
|
dataIndex: 'id',
|
|
hidden: true,
|
|
},
|
|
{
|
|
header: gettext('Node'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'node',
|
|
renderer: function(value) {
|
|
if (value) {
|
|
return value;
|
|
}
|
|
return me.allText;
|
|
},
|
|
},
|
|
{
|
|
header: gettext('Schedule'),
|
|
width: 150,
|
|
dataIndex: 'schedule',
|
|
},
|
|
{
|
|
text: gettext('Next Run'),
|
|
dataIndex: 'next-run',
|
|
width: 150,
|
|
renderer: PVE.Utils.render_next_event,
|
|
},
|
|
{
|
|
header: gettext('Storage'),
|
|
width: 100,
|
|
sortable: true,
|
|
dataIndex: 'storage',
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
],
|
|
listeners: {
|
|
activate: reload,
|
|
itemdblclick: run_editor,
|
|
},
|
|
});
|
|
|
|
me.callParent();
|
|
},
|
|
}, function() {
|
|
Ext.define('pve-cluster-backup', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'id',
|
|
'compress',
|
|
'dow',
|
|
'exclude',
|
|
'mailto',
|
|
'mode',
|
|
'node',
|
|
'pool',
|
|
'prune-backups',
|
|
'starttime',
|
|
'storage',
|
|
'vmid',
|
|
{ name: 'enabled', type: 'boolean' },
|
|
{ name: 'all', type: 'boolean' },
|
|
],
|
|
});
|
|
});
|