diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 0c266ef7f..4bfc5fd0e 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -70,6 +70,7 @@ JSSRC= \ form/GlobalSearchField.js \ form/QemuBiosSelector.js \ form/VMSelector.js \ + form/USBSelector.js \ dc/Tasks.js \ dc/Log.js \ panel/StatusPanel.js \ @@ -158,6 +159,7 @@ JSSRC= \ qemu/SnapshotTree.js \ qemu/Config.js \ qemu/CreateWizard.js \ + qemu/USBEdit.js \ lxc/Summary.js \ lxc/Network.js \ lxc/Resources.js \ diff --git a/www/manager6/form/USBSelector.js b/www/manager6/form/USBSelector.js new file mode 100644 index 000000000..bc09b8d64 --- /dev/null +++ b/www/manager6/form/USBSelector.js @@ -0,0 +1,145 @@ +Ext.define('PVE.form.USBSelector', { + extend: 'PVE.form.ComboGrid', + alias: ['widget.pveUSBSelector'], + allowBlank: false, + autoSelect: false, + displayField: 'usbid', + valueField: 'usbid', + editable: true, + + getUSBValue: function() { + var me = this; + var rec = me.store.findRecord('usbid', me.value); + var val = 'host='+ me.value; + if (rec && rec.data.speed === "5000") { + val = 'host=' + me.value + ",usb3=1"; + } + return val; + }, + + validator: function(value) { + var me = this; + if (me.type === 'device') { + return (/^[a-f0-9]{4}\:[a-f0-9]{4}$/i).test(value); + } else if (me.type === 'port') { + return (/^[0-9]+\-[0-9]+(\.[0-9]+)*$/).test(value); + } + return false; + }, + + initComponent: function() { + var me = this; + + var nodename = me.pveSelNode.data.node; + + if (!nodename) { + throw "no nodename specified"; + } + + if (me.type !== 'device' && me.type !== 'port') { + throw "no valid type specified"; + } + + var store = new Ext.data.Store({ + model: 'pve-usb-' + me.type, + proxy: { + type: 'pve', + url: "/api2/json/nodes/" + nodename + "/scan/usb" + }, + filters: [ + function (item) { + return !!item.data.usbpath && !!item.data.prodid && item.data['class'] != 9; + } + ] + }); + + Ext.apply(me, { + store: store, + listConfig: { + columns: [ + { + header: (me.type === 'device')?gettext('Device'):gettext('Port'), + sortable: true, + dataIndex: 'usbid', + width: 80 + }, + { + header: gettext('Manufacturer'), + sortable: true, + dataIndex: 'manufacturer', + width: 100 + }, + { + header: gettext('Product'), + sortable: true, + dataIndex: 'product', + flex: 1 + }, + { + header: gettext('Speed'), + width: 70, + sortable: true, + dataIndex: 'speed', + renderer: function(value) { + if (value === "5000") { + return "USB 3.0"; + } else if (value === "480") { + return "USB 2.0"; + } else { + return "USB 1.x"; + } + } + } + ] + } + }); + + me.callParent(); + + store.load(); + } + +}, function() { + + Ext.define('pve-usb-device', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'usbid', + convert: function(val, data) { + if (val) { + return val; + } + return data.get('vendid') + ':' + data.get('prodid'); + } + }, + 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath', + { name: 'port' , type: 'number' }, + { name: 'level' , type: 'number' }, + { name: 'class' , type: 'number' }, + { name: 'devnum' , type: 'number' }, + { name: 'busnum' , type: 'number' } + ] + }); + + Ext.define('pve-usb-port', { + extend: 'Ext.data.Model', + fields: [ + { + name: 'usbid', + convert: function(val,data) { + if (val) { + return val; + } + return data.get('busnum') + '-' + data.get('usbpath'); + } + }, + 'speed', 'product', 'manufacturer', 'vendid', 'prodid', 'usbpath', + { name: 'port' , type: 'number' }, + { name: 'level' , type: 'number' }, + { name: 'class' , type: 'number' }, + { name: 'devnum' , type: 'number' }, + { name: 'busnum' , type: 'number' } + ] + }); +}); diff --git a/www/manager6/qemu/USBEdit.js b/www/manager6/qemu/USBEdit.js new file mode 100644 index 000000000..ad3605061 --- /dev/null +++ b/www/manager6/qemu/USBEdit.js @@ -0,0 +1,213 @@ +Ext.define('PVE.qemu.USBInputPanel', { + extend: 'PVE.panel.InputPanel', + + autoComplete: false, + onlineHelp: 'qm_usb_passthrough', + + controller: { + xclass: 'Ext.app.ViewController', + + control: { + 'field[name=usb]': { + change: function(field, newValue, oldValue) { + var hwidfield = this.lookupReference('hwid'); + var portfield = this.lookupReference('port'); + var usb3field = this.lookupReference('usb3'); + if (field.inputValue === 'hostdevice') { + hwidfield.setDisabled(!newValue); + } else if(field.inputValue === 'port') { + portfield.setDisabled(!newValue); + } else if(field.inputValue === 'spice') { + usb3field.setDisabled(newValue); + } + } + }, + 'pveUSBSelector': { + change: function(field, newValue, oldValue) { + var usbval = field.getUSBValue(); + var usb3field = this.lookupReference('usb3'); + var usb3 = /usb3/.test(usbval); + if(usb3 && !usb3field.isDisabled()) { + usb3field.savedVal = usb3field.getValue(); + usb3field.setValue(true); + usb3field.setDisabled(true); + } else if(!usb3 && usb3field.isDisabled()){ + var val = (usb3field.savedVal === undefined)?usb3field.originalValue:usb3field.savedVal; + usb3field.setValue(val); + usb3field.setDisabled(false); + } + } + } + } + }, + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + }, + + onGetValues: function(values) { + var me = this; + if(!me.confid) { + var i; + for (i = 0; i < 6; i++) { + if (!me.vmconfig['usb' + i.toString()]) { + me.confid = 'usb' + i.toString(); + break; + } + } + } + var val = ""; + var type = me.down('radiofield').getGroupValue(); + switch (type) { + case 'spice': + val = 'spice'; break; + case 'hostdevice': + case 'port': + val = me.down('pveUSBSelector[name=' + type + ']').getUSBValue(); + if (!/usb3/.test(val) && me.down('field[name=usb3]').getValue() === true) { + val += ',usb3=1'; + } + break; + default: + throw "invalid type selected"; + } + + values[me.confid] = val; + return values; + }, + + initComponent: function () { + var me = this; + + var items = [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + items:[ + { + name: 'usb', + inputValue: 'spice', + boxLabel: gettext('Spice Port'), + submitValue: false, + checked: true + }, + { + name: 'usb', + inputValue: 'hostdevice', + boxLabel: gettext('Use USB Vendor/Device ID'), + submitValue: false + }, + { + xtype: 'pveUSBSelector', + disabled: true, + type: 'device', + name: 'hostdevice', + pveSelNode: me.pveSelNode, + editable: true, + reference: 'hwid', + allowBlank: false, + fieldLabel: 'Choose Device', + labelAlign: 'right', + submitValue: false + }, + { + name: 'usb', + inputValue: 'port', + boxLabel: gettext('Use USB Port'), + submitValue: false + }, + { + xtype: 'pveUSBSelector', + disabled: true, + name: 'port', + pveSelNode: me.pveSelNode, + editable: true, + type: 'port', + reference: 'port', + allowBlank: false, + fieldLabel: gettext('Choose Port'), + labelAlign: 'right', + submitValue: false + }, + { + xtype: 'checkbox', + name: 'usb3', + submitValue: false, + reference: 'usb3', + fieldLabel: gettext('Use USB3') + } + ] + } + ]; + + Ext.apply(me, { + items: items + }); + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.USBEdit', { + extend: 'PVE.window.Edit', + + vmconfig: undefined, + + isAdd: true, + + subject: gettext('USB Device'), + + + initComponent : function() { + var me = this; + + me.isCreate = !me.confid; + + var ipanel = Ext.create('PVE.qemu.USBInputPanel', { + confid: me.confid, + pveSelNode: me.pveSelNode + }); + + Ext.apply(me, { + items: [ ipanel ] + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + ipanel.setVMConfig(response.result.data); + if (me.confid) { + var data = response.result.data[me.confid].split(','); + var port, hostdevice, usb3 = false; + var type = 'spice'; + var i; + for (i = 0; i < data.length; i++) { + if (/^(host=)?(0x)?[a-zA-Z0-9]{4}\:(0x)?[a-zA-Z0-9]{4}$/.test(data[i])) { + hostdevice = data[i]; + hostdevice = hostdevice.replace('host=', '').replace('0x',''); + type = 'hostdevice'; + } else if (/^(host=)?(\d+)\-(\d+(\.\d+)*)$/.test(data[i])) { + port = data[i]; + port = port.replace('host=',''); + type = 'port'; + } + + if (/^usb3=(1|on|true)$/.test(data[i])) { + usb3 = true; + } + } + var values = { + usb : type, + hostdevice: hostdevice, + port: port, + usb3: usb3 + }; + + ipanel.setValues(values); + } + } + }); + } +});