pve-manager/www/manager6/window/Migrate.js
Aaron Lauterer d27a44a615 ui: fix column behavior with browser scaling
especially on chromium based browser (e.g. chrome, edge) it can happen,
depending on the zoom level, that the last column does not fit next to
the other columns and is moved below the other columns.

This results in an ugly looking UI and in the worst case makes it
unusable.

This can also be triggered if the monitor is set to a higher scaling /
different DPI settings. I was able to have the same problem in Edge when
setting the scaling in the windows display settings to 125% (Clone VM).

Changing the layout from columns with 0.5 width to extjs HBOXes with
flex 1 works as expected.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2020-11-17 09:58:49 +01:00

437 lines
11 KiB
JavaScript

Ext.define('PVE.window.Migrate', {
extend: 'Ext.window.Window',
vmtype: undefined,
nodename: undefined,
vmid: undefined,
maxHeight: 450,
viewModel: {
data: {
vmid: undefined,
nodename: undefined,
vmtype: undefined,
running: false,
qemu: {
onlineHelp: 'qm_migration',
commonName: 'VM'
},
lxc: {
onlineHelp: 'pct_migration',
commonName: 'CT'
},
migration: {
possible: true,
preconditions: [],
'with-local-disks': 0,
mode: undefined,
allowedNodes: undefined,
overwriteLocalResourceCheck: false,
hasLocalResources: false
}
},
formulas: {
setMigrationMode: function(get) {
if (get('running')){
if (get('vmtype') === 'qemu') {
return gettext('Online');
} else {
return gettext('Restart Mode');
}
} else {
return gettext('Offline');
}
},
setStorageselectorHidden: function(get) {
if (get('migration.with-local-disks') && get('running')) {
return false;
} else {
return true;
}
},
setLocalResourceCheckboxHidden: function(get) {
if (get('running') || !get('migration.hasLocalResources') ||
Proxmox.UserName !== 'root@pam') {
return true;
} else {
return false;
}
}
}
},
controller: {
xclass: 'Ext.app.ViewController',
control: {
'panel[reference=formPanel]': {
validityChange: function(panel, isValid) {
this.getViewModel().set('migration.possible', isValid);
this.checkMigratePreconditions();
}
}
},
init: function(view) {
var me = this,
vm = view.getViewModel();
if (!view.nodename) {
throw "missing custom view config: nodename";
}
vm.set('nodename', view.nodename);
if (!view.vmid) {
throw "missing custom view config: vmid";
}
vm.set('vmid', view.vmid);
if (!view.vmtype) {
throw "missing custom view config: vmtype";
}
vm.set('vmtype', view.vmtype);
view.setTitle(
Ext.String.format('{0} {1} {2}', gettext('Migrate'), vm.get(view.vmtype).commonName, view.vmid)
);
me.lookup('proxmoxHelpButton').setHelpConfig({
onlineHelp: vm.get(view.vmtype).onlineHelp
});
me.checkMigratePreconditions();
me.lookup('formPanel').isValid();
},
onTargetChange: function (nodeSelector) {
//Always display the storages of the currently seleceted migration target
this.lookup('pveDiskStorageSelector').setNodename(nodeSelector.value);
this.checkMigratePreconditions();
},
startMigration: function() {
var me = this,
view = me.getView(),
vm = me.getViewModel();
var values = me.lookup('formPanel').getValues();
var params = {
target: values.target
};
if (vm.get('migration.mode')) {
params[vm.get('migration.mode')] = 1;
}
if (vm.get('migration.with-local-disks')) {
params['with-local-disks'] = 1;
}
//offline migration to a different storage currently might fail at a late stage
//(i.e. after some disks have been moved), so don't expose it yet in the GUI
if (vm.get('migration.with-local-disks') && vm.get('running') && values.targetstorage) {
params.targetstorage = values.targetstorage;
}
if (vm.get('migration.overwriteLocalResourceCheck')) {
params['force'] = 1;
}
Proxmox.Utils.API2Request({
params: params,
url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
waitMsgTarget: view,
method: 'POST',
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, options) {
var upid = response.result.data;
var extraTitle = Ext.String.format(' ({0} ---> {1})', vm.get('nodename'), params.target);
Ext.create('Proxmox.window.TaskViewer', {
upid: upid,
extraTitle: extraTitle
}).show();
view.close();
}
});
},
checkMigratePreconditions: function(resetMigrationPossible) {
var me = this,
vm = me.getViewModel();
var vmrec = PVE.data.ResourceStore.findRecord('vmid', vm.get('vmid'),
0, false, false, true);
if (vmrec && vmrec.data && vmrec.data.running) {
vm.set('running', true);
}
if (vm.get('vmtype') === 'qemu') {
me.checkQemuPreconditions(resetMigrationPossible);
} else {
me.checkLxcPreconditions(resetMigrationPossible);
}
me.lookup('pveNodeSelector').disallowedNodes = [vm.get('nodename')];
// Only allow nodes where the local storage is available in case of offline migration
// where storage migration is not possible
me.lookup('pveNodeSelector').allowedNodes = vm.get('migration.allowedNodes');
me.lookup('formPanel').isValid();
},
checkQemuPreconditions: function(resetMigrationPossible) {
var me = this,
vm = me.getViewModel(),
migrateStats;
if (vm.get('running')) {
vm.set('migration.mode', 'online');
}
Proxmox.Utils.API2Request({
url: '/nodes/' + vm.get('nodename') + '/' + vm.get('vmtype') + '/' + vm.get('vmid') + '/migrate',
method: 'GET',
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, options) {
migrateStats = response.result.data;
if (migrateStats.running) {
vm.set('running', true);
}
// Get migration object from viewmodel to prevent
// to many bind callbacks
var migration = vm.get('migration');
if (resetMigrationPossible) migration.possible = true;
migration.preconditions = [];
if (migrateStats.allowed_nodes) {
migration.allowedNodes = migrateStats.allowed_nodes;
var target = me.lookup('pveNodeSelector').value;
if (target.length && !migrateStats.allowed_nodes.includes(target)) {
let disallowed = migrateStats.not_allowed_nodes[target];
let missing_storages = disallowed.unavailable_storages.join(', ');
migration.possible = false;
migration.preconditions.push({
text: 'Storage (' + missing_storages + ') not available on selected target. ' +
'Start VM to use live storage migration or select other target node',
severity: 'error'
});
}
}
if (migrateStats.local_resources.length) {
migration.hasLocalResources = true;
if(!migration.overwriteLocalResourceCheck || vm.get('running')){
migration.possible = false;
migration.preconditions.push({
text: Ext.String.format('Can\'t migrate VM with local resources: {0}',
migrateStats.local_resources.join(', ')),
severity: 'error'
});
} else {
migration.preconditions.push({
text: Ext.String.format('Migrate VM with local resources: {0}. ' +
'This might fail if resources aren\'t available on the target node.',
migrateStats.local_resources.join(', ')),
severity: 'warning'
});
}
}
if (migrateStats.local_disks.length) {
migrateStats.local_disks.forEach(function (disk) {
if (disk.cdrom && disk.cdrom === 1) {
if (disk.volid.includes('vm-'+vm.get('vmid')+'-cloudinit')) {
if (migrateStats.running) {
migration.possible = false;
migration.preconditions.push({
text: "Can't live migrate VM with local cloudinit disk, use shared storage instead",
severity: 'error'
});
} else {
return;
}
} else {
migration.possible = false;
migration.preconditions.push({
text: "Can't migrate VM with local CD/DVD",
severity: 'error'
});
}
} else {
var size_string = disk.size ? '(' + PVE.Utils.render_size(disk.size) + ')' : '';
migration['with-local-disks'] = 1;
migration.preconditions.push({
text: Ext.String.format('Migration with local disk might take long: {0} {1}',
disk.volid, size_string),
severity: 'warning'
});
}
});
}
vm.set('migration', migration);
}
});
},
checkLxcPreconditions: function(resetMigrationPossible) {
var me = this,
vm = me.getViewModel();
if (vm.get('running')) {
vm.set('migration.mode', 'restart');
}
}
},
width: 600,
modal: true,
layout: {
type: 'vbox',
align: 'stretch'
},
border: false,
items: [
{
xtype: 'form',
reference: 'formPanel',
bodyPadding: 10,
border: false,
layout: 'hbox',
items: [
{
xtype: 'container',
flex: 1,
items: [{
xtype: 'displayfield',
name: 'source',
fieldLabel: gettext('Source node'),
bind: {
value: '{nodename}'
}
},
{
xtype: 'displayfield',
reference: 'migrationMode',
fieldLabel: gettext('Mode'),
bind: {
value: '{setMigrationMode}'
}
}]
},
{
xtype: 'container',
flex: 1,
items: [{
xtype: 'pveNodeSelector',
reference: 'pveNodeSelector',
name: 'target',
fieldLabel: gettext('Target node'),
allowBlank: false,
disallowedNodes: undefined,
onlineValidator: true,
listeners: {
change: 'onTargetChange'
}
},
{
xtype: 'pveStorageSelector',
reference: 'pveDiskStorageSelector',
name: 'targetstorage',
fieldLabel: gettext('Target storage'),
storageContent: 'images',
allowBlank: true,
autoSelect: false,
emptyText: gettext('Current layout'),
bind: {
hidden: '{setStorageselectorHidden}'
}
},
{
xtype: 'proxmoxcheckbox',
name: 'overwriteLocalResourceCheck',
fieldLabel: gettext('Force'),
autoEl: {
tag: 'div',
'data-qtip': 'Overwrite local resources unavailable check'
},
bind: {
hidden: '{setLocalResourceCheckboxHidden}',
value: '{migration.overwriteLocalResourceCheck}'
},
listeners: {
change: {fn: 'checkMigratePreconditions', extraArg: true}
}
}]
}
]
},
{
xtype: 'gridpanel',
reference: 'preconditionGrid',
selectable: false,
flex: 1,
columns: [{
text: '',
dataIndex: 'severity',
renderer: function(v) {
switch (v) {
case 'warning':
return '<i class="fa fa-exclamation-triangle warning"></i> ';
case 'error':
return '<i class="fa fa-times critical"></i>';
default:
return v;
}
},
width: 35
},
{
text: 'Info',
dataIndex: 'text',
cellWrap: true,
flex: 1
}],
bind: {
hidden: '{!migration.preconditions.length}',
store: {
fields: ['severity', 'text'],
data: '{migration.preconditions}',
sorters: 'text',
}
}
}
],
buttons: [
{
xtype: 'proxmoxHelpButton',
reference: 'proxmoxHelpButton',
onlineHelp: 'pct_migration',
listenToGlobalEvent: false,
hidden: false
},
'->',
{
xtype: 'button',
reference: 'submitButton',
text: gettext('Migrate'),
handler: 'startMigration',
bind: {
disabled: '{!migration.possible}'
}
}
]
});