Upstream proxmox-git/pve-manager state of the touched files before 9545f20670
This commit is contained in:
1 parent
85bc6aeedb
commit
a982dab632
1 file changed
+980
@@ -0,0 +1,980 @@
|
||||
Ext.define('PVE.dc.BackupEdit', {
|
||||
extend: 'Proxmox.window.Edit',
|
||||
alias: ['widget.pveDcBackupEdit'],
|
||||
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
defaultFocus: undefined,
|
||||
|
||||
subject: gettext('Backup Job'),
|
||||
width: 720,
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
me.resetSearch();
|
||||
|
||||
store.clearFilter();
|
||||
store.filterBy(function (rec) {
|
||||
return !value || rec.get('node') === value;
|
||||
});
|
||||
|
||||
if (value) {
|
||||
let selModel = vmgrid.getSelectionModel();
|
||||
let selections = selModel.getSelection();
|
||||
let hiddenSelections = selections.filter((rec) => rec.get('node') !== value);
|
||||
if (hiddenSelections.length > 0) {
|
||||
selModel.deselect(hiddenSelections, true);
|
||||
}
|
||||
}
|
||||
|
||||
let mode = me.lookup('modeSelector').getValue();
|
||||
if (mode === 'all') {
|
||||
vmgrid.selModel.selectAll(true);
|
||||
}
|
||||
if (mode === 'pool') {
|
||||
me.selectPoolMembers();
|
||||
}
|
||||
|
||||
me.updateSelectionCount();
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
me.updateSelectionCount();
|
||||
},
|
||||
|
||||
modeChange: function (f, value, oldValue) {
|
||||
let me = this;
|
||||
let vmgrid = me.lookup('vmgrid');
|
||||
vmgrid.getStore().removeFilter('poolFilter');
|
||||
|
||||
me.resetSearch();
|
||||
|
||||
if (
|
||||
(oldValue === 'all' && value !== 'all') ||
|
||||
(oldValue === 'pool' && (value === 'include' || value === 'exclude'))
|
||||
) {
|
||||
vmgrid.getSelectionModel().deselectAll(true);
|
||||
}
|
||||
|
||||
if (value === 'all') {
|
||||
vmgrid.getSelectionModel().selectAll(true);
|
||||
}
|
||||
|
||||
if (value === 'pool') {
|
||||
me.selectPoolMembers();
|
||||
}
|
||||
|
||||
me.updateSelectionCount();
|
||||
},
|
||||
|
||||
compressionChange: function (f, value, oldValue) {
|
||||
this.getView().lookup('backupAdvanced').updateCompression(value, f.isDisabled());
|
||||
},
|
||||
|
||||
compressionDisable: function (f) {
|
||||
this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), true);
|
||||
},
|
||||
|
||||
compressionEnable: function (f) {
|
||||
this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), false);
|
||||
},
|
||||
|
||||
prepareValues: function (data) {
|
||||
let me = this;
|
||||
let viewModel = me.getViewModel();
|
||||
|
||||
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'];
|
||||
}
|
||||
|
||||
if (data['notes-template']) {
|
||||
data['notes-template'] = PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
|
||||
}
|
||||
|
||||
if (data.performance) {
|
||||
Object.assign(data, data.performance);
|
||||
delete data.performance;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
resetSearch: function () {
|
||||
this.lookup('searchField').setValue('');
|
||||
},
|
||||
|
||||
selectionChange: function (_, selected) {
|
||||
let me = this;
|
||||
let store = me.lookup('vmgrid').getStore();
|
||||
|
||||
// re-apply the review filter to drop just-deselected rows from view
|
||||
if (store.getFilters().contains(me.reviewFilter)) {
|
||||
store.removeFilter(me.reviewFilter);
|
||||
store.addFilter(me.reviewFilter);
|
||||
}
|
||||
me.updateSelectionCount(selected);
|
||||
},
|
||||
|
||||
updateSelectionCount: function (selected) {
|
||||
let me = this;
|
||||
let selection = selected || me.lookup('vmgrid').getSelectionModel().getSelection();
|
||||
let count = selection.length;
|
||||
|
||||
let label = me.lookup('selectionCount');
|
||||
let text = Ext.String.format(gettext('Selected ({0})'), count);
|
||||
label.setText(text);
|
||||
},
|
||||
|
||||
reviewFn: function (record) {
|
||||
let me = this;
|
||||
return me.lookup('vmgrid').getSelectionModel().isSelected(record);
|
||||
},
|
||||
|
||||
reviewModeChange: function (_, value) {
|
||||
let me = this;
|
||||
let store = me.lookup('vmgrid').getStore();
|
||||
|
||||
me.resetSearch();
|
||||
if (value) {
|
||||
store.addFilter(me.reviewFilter);
|
||||
} else {
|
||||
store.removeFilter(me.reviewFilter);
|
||||
}
|
||||
},
|
||||
|
||||
init: function (view) {
|
||||
let me = this;
|
||||
|
||||
me.reviewFilter = new Ext.util.Filter({
|
||||
id: 'vmgrid-review',
|
||||
scope: me,
|
||||
filterFn: me.reviewFn,
|
||||
});
|
||||
me.lookup('searchField').setTargetStore(me.lookup('vmgrid').getStore());
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
viewModel: {
|
||||
data: {
|
||||
selMode: 'include',
|
||||
},
|
||||
|
||||
formulas: {
|
||||
poolMode: (get) => get('selMode') === 'pool',
|
||||
disableVMSelection: (get) =>
|
||||
get('selMode') !== 'include' && get('selMode') !== 'exclude',
|
||||
selectionLabel: function (get) {
|
||||
let mode = get('selMode');
|
||||
if (mode === 'include') {
|
||||
return gettext('Guests to Include');
|
||||
} else if (mode === 'exclude') {
|
||||
return gettext('Guests to Exclude');
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'tabpanel',
|
||||
region: 'center',
|
||||
layout: 'fit',
|
||||
bodyPadding: 10,
|
||||
items: [
|
||||
{
|
||||
title: gettext('General'),
|
||||
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: '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,
|
||||
padding: '0 0 2 0',
|
||||
name: 'vmid',
|
||||
disabled: true,
|
||||
allowBlank: false,
|
||||
columnSelection: ['vmid', 'node', 'status', 'name', 'type'],
|
||||
bind: {
|
||||
disabled: '{disableVMSelection}',
|
||||
},
|
||||
listeners: {
|
||||
selectionChange: 'selectionChange',
|
||||
},
|
||||
// override to keep selections hidden by the search/review filters
|
||||
// and to return the joined string the backup API expects.
|
||||
getValue: function () {
|
||||
let me = this;
|
||||
let selection = me.getSelectionModel().getSelection();
|
||||
return selection.map((rec) => rec.get('vmid')).join(',');
|
||||
},
|
||||
tbar: [
|
||||
{
|
||||
xtype: 'tbtext',
|
||||
bind: {
|
||||
text: '{selectionLabel}',
|
||||
hidden: '{!selectionLabel}',
|
||||
},
|
||||
},
|
||||
'->',
|
||||
gettext('Search') + ':',
|
||||
' ',
|
||||
{
|
||||
xtype: 'pveRecordSearchField',
|
||||
reference: 'searchField',
|
||||
emptyText: gettext('Name, VMID, Type'),
|
||||
searchFields: ['name', 'vmid', 'type'],
|
||||
},
|
||||
],
|
||||
bbar: {
|
||||
xtype: 'toolbar',
|
||||
padding: '4 0',
|
||||
items: [
|
||||
{
|
||||
xtype: 'tbtext',
|
||||
reference: 'selectionCount',
|
||||
text: Ext.String.format(gettext('Selected ({0})'), 0),
|
||||
},
|
||||
'->',
|
||||
{
|
||||
xtype: 'proxmoxcheckbox',
|
||||
boxLabel: gettext('Review'),
|
||||
submitValue: false,
|
||||
listeners: {
|
||||
change: 'reviewModeChange',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
onGetValues: function (values) {
|
||||
return this.up('window').getController().onGetValues(values);
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'pveBackupNotificationOptionsPanel',
|
||||
title: gettext('Notifications'),
|
||||
cbind: {
|
||||
isCreate: '{isCreate}',
|
||||
},
|
||||
},
|
||||
{
|
||||
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',
|
||||
onlineHelp: 'chapter_vzdump',
|
||||
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',
|
||||
onlineHelp: 'chapter_vzdump',
|
||||
reference: 'backupAdvanced',
|
||||
title: gettext('Advanced'),
|
||||
cbind: {
|
||||
isCreate: '{isCreate}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Ext.create('PVE.dc.BackupEdit', {
|
||||
autoShow: true,
|
||||
jobid: rec.data.id,
|
||||
listeners: {
|
||||
destroy: () => reload(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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', '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 = [];
|
||||
|
||||
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',
|
||||
renderer: Proxmox.Utils.renderEnabledIcon,
|
||||
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' },
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user