[pve-devel] [PATCH v6 manager 3/5] ui: lxc/qemu: add disk reassign and action submenu
Aaron Lauterer
a.lauterer at proxmox.com
Tue Apr 5 14:30:14 CEST 2022
For the new HDReassign component, we follow the approach of HDMove to
have one componend for qemu and lxc.
To avoid button clutter, a new "Disk/Volume action" button is
introduced. It holds the Move, Reassign and Resize buttons in a submenu.
Signed-off-by: Aaron Lauterer <a.lauterer at proxmox.com>
---
changes since
v5: rebased
v4:
* use button labels and icons as suggested by @Thomas
only for Move Storage I went with the same Icon we use for the
storages. That should make the connection clearer since we are also
using the LXC/VM icons for the reassign buttons.
* smaller style nits like reordering button/menuitem definitions to be
grouped closer together
* reworked logic when to disable buttons/menuitems
v3:
* use PVE.Util.nextFreeMP
* create 'url' in one place, no need for submitURL
* filter out templates in target guest selection
* fix padding
* code cleanup changes
v2:
* switch from generic to Proxmox Edit window
* add new submenu for disk/volume specific actions
* code style improvements
* simplify some labels, removing "disk" and "volume" as the context
already gives this away
v1: incorporated feedback I got off list
www/manager6/Makefile | 1 +
www/manager6/lxc/Resources.js | 78 +++++++--
www/manager6/qemu/HDReassign.js | 272 ++++++++++++++++++++++++++++++
www/manager6/qemu/HardwareView.js | 74 ++++++--
4 files changed, 391 insertions(+), 34 deletions(-)
create mode 100644 www/manager6/qemu/HDReassign.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index e6e01bd1..a7101553 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -213,6 +213,7 @@ JSSRC= \
qemu/HDEfi.js \
qemu/HDTPM.js \
qemu/HDMove.js \
+ qemu/HDReassign.js \
qemu/HDResize.js \
qemu/HardwareView.js \
qemu/IPConfigEdit.js \
diff --git a/www/manager6/lxc/Resources.js b/www/manager6/lxc/Resources.js
index 683be526..9ca33f17 100644
--- a/www/manager6/lxc/Resources.js
+++ b/www/manager6/lxc/Resources.js
@@ -162,7 +162,8 @@ Ext.define('PVE.lxc.RessourceView', {
});
};
- var run_move = function(b, e, rec) {
+ let run_move = function() {
+ let rec = me.selModel.getSelection()[0];
if (!rec) {
return;
}
@@ -179,6 +180,24 @@ Ext.define('PVE.lxc.RessourceView', {
win.on('destroy', me.reload, me);
};
+ let run_reassign = function() {
+ let rec = me.selModel.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ Ext.create('PVE.window.HDReassign', {
+ disk: rec.data.key,
+ nodename: nodename,
+ autoShow: true,
+ vmid: vmid,
+ type: 'lxc',
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ };
+
var edit_btn = new Proxmox.button.Button({
text: gettext('Edit'),
selModel: me.selModel,
@@ -193,13 +212,6 @@ Ext.define('PVE.lxc.RessourceView', {
handler: function() { me.run_editor(); },
});
- var resize_btn = new Proxmox.button.Button({
- text: gettext('Resize disk'),
- selModel: me.selModel,
- disabled: true,
- handler: run_resize,
- });
-
var remove_btn = new Proxmox.button.Button({
text: gettext('Remove'),
defaultText: gettext('Remove'),
@@ -238,14 +250,41 @@ Ext.define('PVE.lxc.RessourceView', {
},
});
- var move_btn = new Proxmox.button.Button({
- text: gettext('Move Volume'),
+ let move_menuitem = new Ext.menu.Item({
+ text: gettext('Move Storage'),
+ tooltip: gettext('Move volume to another storage'),
+ iconCls: 'fa fa-database',
selModel: me.selModel,
- disabled: true,
- dangerous: true,
handler: run_move,
});
+ let reassign_menuitem = new Ext.menu.Item({
+ text: gettext('Reassign Owner'),
+ tooltip: gettext('Reassign volume to another CT'),
+ iconCls: 'fa fa-cube',
+ handler: run_reassign,
+ reference: 'reassing_item',
+ });
+
+ let resize_menuitem = new Ext.menu.Item({
+ text: gettext('Resize'),
+ iconCls: 'fa fa-plus',
+ selModel: me.selModel,
+ handler: run_resize,
+ });
+
+ let volumeaction_btn = new Proxmox.button.Button({
+ text: gettext('Volume Action'),
+ disabled: true,
+ menu: {
+ items: [
+ move_menuitem,
+ reassign_menuitem,
+ resize_menuitem,
+ ],
+ },
+ });
+
var revert_btn = new PVE.button.PendingRevert();
var set_button_status = function() {
@@ -254,7 +293,7 @@ Ext.define('PVE.lxc.RessourceView', {
if (!rec) {
edit_btn.disable();
remove_btn.disable();
- resize_btn.disable();
+ volumeaction_btn.disable();
revert_btn.disable();
return;
}
@@ -264,6 +303,7 @@ Ext.define('PVE.lxc.RessourceView', {
var pending = rec.data.delete || me.hasPendingChanges(key);
var isDisk = rowdef.tdCls === 'pve-itype-icon-storage';
+ let isRootFS = rec.data.key === 'rootfs';
var isUnusedDisk = key.match(/^unused\d+/);
var isUsedDisk = isDisk && !isUnusedDisk;
@@ -276,9 +316,12 @@ Ext.define('PVE.lxc.RessourceView', {
}
edit_btn.setDisabled(noedit);
- remove_btn.setDisabled(!isDisk || rec.data.key === 'rootfs' || !diskCap || pending);
- resize_btn.setDisabled(!isDisk || !diskCap || isUnusedDisk);
- move_btn.setDisabled(!isDisk || !diskCap);
+ volumeaction_btn.setDisabled(!isDisk || !diskCap);
+ move_menuitem.setDisabled(isUnusedDisk);
+ reassign_menuitem.setDisabled(isRootFS);
+ resize_menuitem.setDisabled(isUnusedDisk);
+
+ remove_btn.setDisabled(!isDisk || isRootFS || !diskCap || pending);
revert_btn.setDisabled(!pending);
remove_btn.setText(isUsedDisk ? remove_btn.altText : remove_btn.defaultText);
@@ -340,8 +383,7 @@ Ext.define('PVE.lxc.RessourceView', {
},
edit_btn,
remove_btn,
- resize_btn,
- move_btn,
+ volumeaction_btn,
revert_btn,
],
rows: rows,
diff --git a/www/manager6/qemu/HDReassign.js b/www/manager6/qemu/HDReassign.js
new file mode 100644
index 00000000..b6c67964
--- /dev/null
+++ b/www/manager6/qemu/HDReassign.js
@@ -0,0 +1,272 @@
+Ext.define('PVE.window.HDReassign', {
+ extend: 'Proxmox.window.Edit',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ resizable: false,
+ modal: true,
+ width: 350,
+ border: false,
+ layout: 'fit',
+ showReset: false,
+ showProgress: true,
+ method: 'POST',
+
+ viewModel: {
+ data: {
+ mpType: '',
+ },
+ formulas: {
+ mpMaxCount: get => get('mpType') === 'mp'
+ ? PVE.Utils.mp_counts.mps - 1
+ : PVE.Utils.mp_counts.unused - 1,
+ },
+ },
+
+ cbindData: function() {
+ let me = this;
+ return {
+ vmid: me.vmid,
+ disk: me.disk,
+ isQemu: me.type === 'qemu',
+ nodename: me.nodename,
+ url: () => {
+ let endpoint = me.type === 'qemu' ? 'move_disk' : 'move_volume';
+ return `/nodes/${me.nodename}/${me.type}/${me.vmid}/${endpoint}`;
+ },
+ };
+ },
+
+ cbind: {
+ title: get => get('isQemu') ? gettext('Reassign disk') : gettext('Reassign volume'),
+ submitText: get => get('title'),
+ qemu: '{isQemu}',
+ url: '{url}',
+ },
+
+ getValues: function() {
+ let me = this;
+ let values = me.formPanel.getForm().getValues();
+
+ let params = {
+ vmid: me.vmid,
+ 'target-vmid': values.targetVmid,
+ };
+
+ params[me.qemu ? 'disk' : 'volume'] = me.disk;
+
+ if (me.qemu) {
+ params['target-disk'] = `${values.controller}${values.deviceid}`;
+ } else {
+ params['target-volume'] = `${values.mpType}${values.mpId}`;
+ }
+ return params;
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ initViewModel: function(model) {
+ let view = this.getView();
+ let mpTypeValue = view.disk.match(/^unused\d+/) ? 'unused' : 'mp';
+ model.set('mpType', mpTypeValue);
+ },
+
+ onMpTypeChange: function(value) {
+ this.getView().getViewModel().set('mpType', value.getValue());
+ this.getView().lookup('mpIdSelector').validate();
+ },
+
+ onTargetVMChange: function(f, vmid) {
+ let me = this;
+ let view = me.getView();
+ let diskSelector = view.lookup('diskSelector');
+ if (!vmid) {
+ diskSelector.setVMConfig(null);
+ me.VMConfig = null;
+ return;
+ }
+
+ let type = view.qemu ? 'qemu' : 'lxc';
+
+ let url = `/nodes/${view.nodename}/${type}/${vmid}/config`;
+ Proxmox.Utils.API2Request({
+ url: url,
+ method: 'GET',
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ success: function(response, options) {
+ if (view.qemu) {
+ diskSelector.setVMConfig(response.result.data);
+ diskSelector.setDisabled(false);
+ } else {
+ let mpIdSelector = view.lookup('mpIdSelector');
+ let mpType = view.lookup('mpType');
+
+ view.VMConfig = response.result.data;
+
+ mpIdSelector.setValue(
+ PVE.Utils.nextFreeMP(
+ view.getViewModel().get('mpType'),
+ view.VMConfig,
+ ).id,
+ );
+
+ mpType.setDisabled(false);
+ mpIdSelector.setDisabled(false);
+ mpIdSelector.validate();
+ }
+ },
+ });
+ },
+ },
+
+ items: [
+ {
+ xtype: 'form',
+ reference: 'moveFormPanel',
+ border: false,
+ fieldDefaults: {
+ labelWidth: 100,
+ anchor: '100%',
+ },
+ items: [
+ {
+ xtype: 'displayfield',
+ name: 'sourceDisk',
+ fieldLabel: gettext('Source'),
+ cbind: {
+ name: get => get('isQemu') ? 'disk' : 'volume',
+ value: '{disk}',
+ },
+ allowBlank: false,
+ },
+ {
+ xtype: 'vmComboSelector',
+ reference: 'targetVMID',
+ name: 'targetVmid',
+ allowBlank: false,
+ fieldLabel: gettext('Target'),
+ bind: {
+ value: '{targetVMID}',
+ },
+ store: {
+ model: 'PVEResources',
+ autoLoad: true,
+ sorters: 'vmid',
+ cbind: {}, // for nested cbinds
+ filters: [
+ {
+ property: 'type',
+ cbind: {
+ value: get => get('isQemu') ? 'qemu' : 'lxc',
+ },
+ },
+ {
+ property: 'node',
+ cbind: {
+ value: '{nodename}',
+ },
+ },
+ {
+ property: 'vmid',
+ operator: '!=',
+ cbind: {
+ value: '{vmid}',
+ },
+ },
+ {
+ property: 'template',
+ value: 0,
+ },
+ ],
+ },
+ listeners: { change: 'onTargetVMChange' },
+ },
+ {
+ xtype: 'pveControllerSelector',
+ reference: 'diskSelector',
+ withUnused: true,
+ disabled: true,
+ cbind: {
+ hidden: '{!isQemu}',
+ },
+ },
+ {
+ xtype: 'container',
+ layout: 'hbox',
+ cbind: {
+ hidden: '{isQemu}',
+ disabled: '{isQemu}',
+ },
+ items: [
+ {
+ xtype: 'pmxDisplayEditField',
+ cbind: {
+ editable: get => !get('disk').match(/^unused\d+/),
+ value: get => get('disk').match(/^unused\d+/) ? 'unused' : 'mp',
+ },
+ disabled: true,
+ name: 'mpType',
+ reference: 'mpType',
+ fieldLabel: gettext('Add as'),
+ submitValue: true,
+ flex: 4,
+ editConfig: {
+ xtype: 'proxmoxKVComboBox',
+ name: 'mpTypeCombo',
+ reference: 'mpTypeCombo',
+ deleteEmpty: false,
+ cbind: {
+ hidden: '{isQemu}',
+ },
+ comboItems: [
+ ['mp', gettext('Mount Point')],
+ ['unused', gettext('Unused')],
+ ],
+ listeners: { change: 'onMpTypeChange' },
+ },
+ },
+ {
+ xtype: 'proxmoxintegerfield',
+ name: 'mpId',
+ reference: 'mpIdSelector',
+ minValue: 0,
+ flex: 1,
+ allowBlank: false,
+ validateOnChange: true,
+ disabled: true,
+ bind: {
+ maxValue: '{mpMaxCount}',
+ },
+ validator: function(value) {
+ let view = this.up('window');
+ let type = view.getViewModel().get('mpType');
+ if (Ext.isDefined(view.VMConfig[`${type}${value}`])) {
+ return "Mount point is already in use.";
+ }
+ return true;
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+
+ initComponent: function() {
+ let me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.vmid) {
+ throw "no VM ID specified";
+ }
+
+ if (!me.type) {
+ throw "no type specified";
+ }
+
+ me.callParent();
+ },
+});
diff --git a/www/manager6/qemu/HardwareView.js b/www/manager6/qemu/HardwareView.js
index e3506e4d..749d355f 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -377,16 +377,17 @@ Ext.define('PVE.qemu.HardwareView', {
handler: run_editor,
});
- let resize_btn = new Proxmox.button.Button({
- text: gettext('Resize disk'),
+ let move_menuitem = new Ext.menu.Item({
+ text: gettext('Move Storage'),
+ tooltip: gettext('Move disk to another storage'),
+ iconCls: 'fa fa-database',
selModel: sm,
- disabled: true,
handler: () => {
let rec = sm.getSelection()[0];
if (!rec) {
return;
}
- Ext.create('PVE.window.HDResize', {
+ Ext.create('PVE.window.HDMove', {
autoShow: true,
disk: rec.data.key,
nodename: nodename,
@@ -398,20 +399,23 @@ Ext.define('PVE.qemu.HardwareView', {
},
});
- let move_btn = new Proxmox.button.Button({
- text: gettext('Move disk'),
+ let reassign_menuitem = new Ext.menu.Item({
+ text: gettext('Reassign Owner'),
+ tooltip: gettext('Reassign disk to another VM'),
+ iconCls: 'fa fa-desktop',
selModel: sm,
- disabled: true,
handler: () => {
- var rec = sm.getSelection()[0];
+ let rec = sm.getSelection()[0];
if (!rec) {
return;
}
- Ext.create('PVE.window.HDMove', {
+
+ Ext.create('PVE.window.HDReassign', {
autoShow: true,
disk: rec.data.key,
nodename: nodename,
vmid: vmid,
+ type: 'qemu',
listeners: {
destroy: () => me.reload(),
},
@@ -419,6 +423,40 @@ Ext.define('PVE.qemu.HardwareView', {
},
});
+ let resize_menuitem = new Ext.menu.Item({
+ text: gettext('Resize'),
+ iconCls: 'fa fa-plus',
+ selModel: sm,
+ handler: () => {
+ let rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+ Ext.create('PVE.window.HDResize', {
+ autoShow: true,
+ disk: rec.data.key,
+ nodename: nodename,
+ vmid: vmid,
+ listeners: {
+ destroy: () => me.reload(),
+ },
+ });
+ },
+ });
+
+ let diskaction_btn = new Proxmox.button.Button({
+ text: gettext('Disk Action'),
+ disabled: true,
+ menu: {
+ items: [
+ move_menuitem,
+ reassign_menuitem,
+ resize_menuitem,
+ ],
+ },
+ });
+
+
let remove_btn = new Proxmox.button.Button({
text: gettext('Remove'),
defaultText: gettext('Remove'),
@@ -544,8 +582,7 @@ Ext.define('PVE.qemu.HardwareView', {
if (!rec) {
remove_btn.disable();
edit_btn.disable();
- resize_btn.disable();
- move_btn.disable();
+ diskaction_btn.disable();
revert_btn.disable();
return;
}
@@ -572,9 +609,15 @@ Ext.define('PVE.qemu.HardwareView', {
edit_btn.setDisabled(
deleted || !row.editor || isCloudInit || (isCDRom && !cdromCap) || (isDisk && !diskCap));
- resize_btn.setDisabled(pending || !isUsedDisk || !diskCap);
-
- move_btn.setDisabled(pending || !(isUsedDisk || isEfi || tpmMoveable) || !diskCap);
+ diskaction_btn.setDisabled(
+ pending ||
+ !diskCap ||
+ isCloudInit ||
+ !(isDisk || isEfi || tpmMoveable),
+ );
+ move_menuitem.setDisabled(isUnusedDisk);
+ reassign_menuitem.setDisabled(pending || (isEfi || tpmMoveable));
+ resize_menuitem.setDisabled(pending || !isUsedDisk);
revert_btn.setDisabled(!pending);
};
@@ -679,8 +722,7 @@ Ext.define('PVE.qemu.HardwareView', {
},
remove_btn,
edit_btn,
- resize_btn,
- move_btn,
+ diskaction_btn,
revert_btn,
],
rows: rows,
--
2.30.2
More information about the pve-devel
mailing list