From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH manager 7/7] ui: BackupEdit: refactor edit window into declarative style
Date: Mon, 6 Mar 2023 15:23:35 +0100 [thread overview]
Message-ID: <20230306142335.3858041-8-d.csapak@proxmox.com> (raw)
In-Reply-To: <20230306142335.3858041-1-d.csapak@proxmox.com>
simplifies some things, e.g. en/disabling the grid and pool selector
while refactoring, cleanup up some smaller things like nested if/else
paths, unnecessary splitting of the deprecated 'dow' parameter, etc.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
www/manager6/dc/Backup.js | 690 ++++++++++++++++++++------------------
1 file changed, 361 insertions(+), 329 deletions(-)
diff --git a/www/manager6/dc/Backup.js b/www/manager6/dc/Backup.js
index d5e8bf20c..d0046177a 100644
--- a/www/manager6/dc/Backup.js
+++ b/www/manager6/dc/Backup.js
@@ -2,72 +2,107 @@ Ext.define('PVE.dc.BackupEdit', {
extend: 'Proxmox.window.Edit',
alias: ['widget.pveDcBackupEdit'],
+ mixins: ['Proxmox.Mixin.CBind'],
+
defaultFocus: undefined,
- initComponent: function() {
- let me = this;
+ subject: gettext("Backup Job"),
+ bodyPadding: 0,
- me.isCreate = !me.jobid;
+ url: '/api2/extjs/cluster/backup',
+ method: 'POST',
+ isCreate: true,
- let url, method;
- if (me.isCreate) {
- url = '/api2/extjs/cluster/backup';
- method = 'POST';
- } else {
- url = '/api2/extjs/cluster/backup/' + me.jobid;
- method = 'PUT';
+ 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
- let 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',
+ 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 storagesel = Ext.create('PVE.form.StorageSelector', {
- fieldLabel: gettext('Storage'),
- clusterView: true,
- 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);
- }
- },
- },
- });
+ if (!values.id && isCreate) {
+ values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
+ }
- let vmgrid = Ext.createWidget('vmselector', {
- height: 300,
- name: 'vmid',
- disabled: true,
- allowBlank: false,
- columnSelection: ['vmid', 'node', 'status', 'name', 'type'],
- });
+ 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 vmgrid = me.lookup('vmgrid');
+ let poolid = me.lookup('poolSelector').getValue();
- let selectPoolMembers = function(poolid) {
+ vmgrid.getSelectionModel().deselectAll(true);
if (!poolid) {
return;
}
- vmgrid.selModel.deselectAll(true);
vmgrid.getStore().filter([
{
id: 'poolFilter',
@@ -76,312 +111,309 @@ Ext.define('PVE.dc.BackupEdit', {
},
]);
vmgrid.selModel.selectAll(true);
- };
+ },
- let 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);
- },
- },
- });
+ modeChange: function(f, value, oldValue) {
+ let me = this;
+ let vmgrid = me.lookup('vmgrid');
+ vmgrid.getStore().removeFilter('poolFilter');
- let 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);
- let mode = selModeField.getValue();
- let store = vmgrid.getStore();
- store.clearFilter();
- store.filterBy(function(rec) {
- return !value || rec.get('node') === value;
- });
- if (mode === 'all') {
- vmgrid.selModel.selectAll(true);
- }
- if (mode === 'pool') {
- selectPoolMembers(selPool.value);
- }
- },
- },
- });
+ if (oldValue === 'all' && value !== 'all') {
+ vmgrid.getSelectionModel().deselectAll(true);
+ }
- let column1 = [
- nodesel,
- storagesel,
- {
- xtype: 'pveCalendarEvent',
- fieldLabel: gettext('Schedule'),
- allowBlank: false,
- name: 'schedule',
- },
- selModeField,
- selPool,
- ];
-
- let column2 = [
- {
- xtype: 'textfield',
- fieldLabel: gettext('Send email to'),
- name: 'mailto',
- },
- {
- xtype: 'pveEmailNotificationSelector',
- fieldLabel: gettext('Email'),
- name: 'mailnotification',
- deleteEmpty: !me.isCreate,
- value: me.isCreate ? 'always' : '',
- },
- {
- xtype: 'pveCompressionSelector',
- fieldLabel: gettext('Compression'),
- name: 'compress',
- deleteEmpty: !me.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,
- },
- ];
+ if (value === 'all') {
+ vmgrid.getSelectionModel().selectAll(true);
+ }
- let ipanel = Ext.create('Proxmox.panel.InputPanel', {
- onlineHelp: 'chapter_vzdump',
- column1: column1,
- column2: column2,
- columnB: [
- {
- xtype: 'proxmoxtextfield',
- name: 'comment',
- fieldLabel: gettext('Job Comment'),
- deleteEmpty: !me.isCreate,
- autoEl: {
- tag: 'div',
- 'data-qtip': gettext('Description of the job'),
- },
- },
- vmgrid,
- ],
- advancedColumn1: [
- {
- xtype: 'proxmoxcheckbox',
- fieldLabel: gettext('Repeat missed'),
- name: 'repeat-missed',
- uncheckedValue: 0,
- defaultValue: 0,
- deleteDefaultValue: !me.isCreate,
- },
- ],
- onGetValues: function(values) {
- if (!values.node) {
- if (!me.isCreate) {
- Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
- }
- delete values.node;
- }
+ if (value === 'pool') {
+ me.selectPoolMembers();
+ }
+ },
- if (!values.id && me.isCreate) {
- values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
- }
+ 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;
- 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 (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';
+ }
- if (selMode !== 'pool') {
- delete values.pool;
- }
- return values;
- },
- });
+ me.getViewModel().set('selMode', data.selMode);
- selModeField.on('change', function(f, value, oldValue) {
- if (oldValue === 'pool') {
- vmgrid.getStore().removeFilter('poolFilter');
- }
+ 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 (oldValue === 'all' || oldValue === 'pool') {
- vmgrid.selModel.deselectAll(true);
- }
+ if (data['notes-template']) {
+ data['notes-template'] =
+ PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
+ }
- if (value === 'all') {
- vmgrid.selModel.selectAll(true);
- vmgrid.setDisabled(true);
- } else {
- vmgrid.setDisabled(false);
+ view.setValues(data);
+ },
+ });
}
+ },
+ },
- if (value === 'pool') {
- vmgrid.setDisabled(true);
- vmgrid.selModel.deselectAll(true);
- selPool.setVisible(true);
- selPool.setDisabled(false);
- selPool.allowBlank = false;
- selectPoolMembers(selPool.value);
- } else {
- selPool.setVisible(false);
- selPool.setDisabled(true);
- selPool.allowBlank = true;
- }
- });
+ viewModel: {
+ data: {
+ selMode: 'include',
+ },
- Ext.applyIf(me, {
- subject: gettext("Backup Job"),
- url: url,
- method: method,
- bodyPadding: 0,
+ 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: 'tabpanel',
+ xtype: 'container',
+ title: gettext('General'),
region: 'center',
- layout: 'fit',
- bodyPadding: 10,
+ layout: {
+ type: 'vbox',
+ align: 'stretch',
+ },
items: [
- {
- xtype: 'container',
- title: gettext('General'),
- region: 'center',
- layout: {
- type: 'vbox',
- align: 'stretch',
- },
- items: [
- ipanel,
- ],
- },
- {
- xtype: 'pveBackupJobPrunePanel',
- title: gettext('Retention'),
- isCreate: me.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: [
+ 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: 'textarea',
- name: 'notes-template',
- fieldLabel: gettext('Backup Notes'),
- height: 100,
- maxLength: 512,
- deleteEmpty: !me.isCreate,
- value: me.isCreate ? '{{guestname}}' : undefined,
+ xtype: 'vmselector',
+ reference: 'vmgrid',
+ height: 300,
+ name: 'vmid',
+ disabled: true,
+ allowBlank: false,
+ columnSelection: ['vmid', 'node', 'status', 'name', 'type'],
+ bind: {
+ disabled: '{disableVMSelection}',
+ },
},
+ ],
+ advancedColumn1: [
{
- xtype: 'box',
- style: {
- margin: '8px 0px',
- 'line-height': '1.5em',
+ xtype: 'proxmoxcheckbox',
+ fieldLabel: gettext('Repeat missed'),
+ name: 'repeat-missed',
+ uncheckedValue: 0,
+ defaultValue: 0,
+ cbind: {
+ deleteDefaultValue: '{!isCreate}',
},
- 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(', '),
- ),
},
],
+ onGetValues: function(values) {
+ return this.up('window').getController().onGetValues(values);
+ },
},
],
},
- ],
-
- });
-
- me.callParent();
-
- if (me.isCreate) {
- selModeField.setValue('include');
- } else {
- me.load({
- success: function(response, options) {
- let 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';
- }
-
- 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;
+ {
+ 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']);
}
- delete data.maxfiles;
- }
-
- if (data['notes-template']) {
- data['notes-template'] = PVE.Utils.unEscapeNotesTemplate(
- data['notes-template']);
- }
-
- me.setValues(data);
+ 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', {
--
2.30.2
next prev parent reply other threads:[~2023-03-06 14:24 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-03-06 14:23 [pve-devel] [PATCH manager 0/7] backup edit window improvements Dominik Csapak
2023-03-06 14:23 ` [pve-devel] [PATCH manager 1/7] ui: VMSelector: columns customizable Dominik Csapak
2023-03-06 14:23 ` [pve-devel] [PATCH manager 2/7] ui: VMSelector: improve {set, get}Value handling with a loading store Dominik Csapak
2023-03-06 14:23 ` [pve-devel] [PATCH manager 3/7] ui: VMSelector: change from filter to load parameters Dominik Csapak
2023-03-06 14:23 ` [pve-devel] [PATCH manager 4/7] ui: VMSelector: correctly change invalid class on en/disable Dominik Csapak
2023-03-11 17:34 ` Thomas Lamprecht
2023-03-06 14:23 ` [pve-devel] [PATCH manager 5/7] fix #4490: ui: add column filters in Backup Job edit window Dominik Csapak
2023-03-06 14:23 ` [pve-devel] [PATCH manager 6/7] fix #4239: ui: show selected but non-existing vmids in backup edit Dominik Csapak
2023-03-06 14:23 ` Dominik Csapak [this message]
2023-03-14 10:37 ` [pve-devel] applied: [PATCH manager 0/7] backup edit window improvements Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20230306142335.3858041-8-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.