From 8814724567838519fc7e8589b0b91a8512d4405e Mon Sep 17 00:00:00 2001 From: upstream Date: Tue, 29 Sep 2026 16:13:13 +0200 Subject: [PATCH] Upstream proxmox-git/pve-manager state of the touched files before 55c76c2746 --- www/manager6/window/DirMapEdit.js | 208 ++++++++++++++++++++ www/manager6/window/PCIMapEdit.js | 305 ++++++++++++++++++++++++++++++ www/manager6/window/USBMapEdit.js | 252 ++++++++++++++++++++++++ 3 files changed, 765 insertions(+) create mode 100644 www/manager6/window/DirMapEdit.js create mode 100644 www/manager6/window/PCIMapEdit.js create mode 100644 www/manager6/window/USBMapEdit.js diff --git a/www/manager6/window/DirMapEdit.js b/www/manager6/window/DirMapEdit.js new file mode 100644 index 0000000..841ff1f --- /dev/null +++ b/www/manager6/window/DirMapEdit.js @@ -0,0 +1,208 @@ +Ext.define('PVE.window.DirMapEditWindow', { + extend: 'Proxmox.window.Edit', + + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: function (initialConfig) { + let me = this; + me.isCreate = !me.name; + me.method = me.isCreate ? 'POST' : 'PUT'; + me.hideMapping = !!me.entryOnly; + me.hideComment = me.name && !me.entryOnly; + me.hideNodeSelector = me.nodename || me.entryOnly; + me.hideNode = !me.nodename || !me.hideNodeSelector; + return { + name: me.name, + nodename: me.nodename, + }; + }, + + submitUrl: function (_url, data) { + let me = this; + let name = me.isCreate ? '' : me.name; + return `/cluster/mapping/dir/${name}`; + }, + + title: gettext('Add Directory Mapping'), + + onlineHelp: 'resource_mapping', + + method: 'POST', + + controller: { + xclass: 'Ext.app.ViewController', + + onGetValues: function (values) { + let me = this; + let view = me.getView(); + values.node ??= view.nodename; + + let name = values.name; + let description = values.description; + let deletes = values.delete; + + delete values.description; + delete values.name; + delete values.delete; + + let map = []; + if (me.originalMap) { + map = PVE.Parser.filterPropertyStringList( + me.originalMap, + (e) => e.node !== values.node, + ); + } + if (values.path) { + // TODO: Remove this when property string supports quotation of properties + if (!/^\/[^;,=()]+/.test(values.path)) { + let errMsg = + 'Value does not look like a valid absolute path.' + + ' These symbols are currently not allowed in path: ;,=()\n'; + Ext.Msg.alert(gettext('Error'), errMsg); + // prevent sending a broken property string to the API + throw errMsg; + } + map.push(PVE.Parser.printPropertyString(values)); + } + values = { map }; + + if (description) { + values.description = description; + } + if (deletes && !view.isCreate) { + values.delete = deletes; + } + if (view.isCreate) { + values.id = name; + } + + return values; + }, + + onSetValues: function (values) { + let me = this; + let view = me.getView(); + me.originalMap = [...values.map]; + let configuredNodes = []; + PVE.Parser.filterPropertyStringList(values.map, (e) => { + configuredNodes.push(e.node); + if (e.node === view.nodename) { + values = e; + } + return false; + }); + + me.lookup('nodeselector').disallowedNodes = configuredNodes; + + return values; + }, + + init: function (view) { + let _me = this; + + if (!view.nodename) { + //throw "no nodename given"; + } + }, + }, + + items: [ + { + xtype: 'inputpanel', + onGetValues: function (values) { + return this.up('window').getController().onGetValues(values); + }, + + onSetValues: function (values) { + return this.up('window').getController().onSetValues(values); + }, + + columnT: [ + { + xtype: 'displayfield', + reference: 'directory-hint', + columnWidth: 1, + value: 'Make sure the directory exists.', + cbind: { + disabled: '{hideMapping}', + hidden: '{hideMapping}', + }, + userCls: 'pmx-hint', + }, + ], + + column1: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + cbind: { + editable: '{!name}', + value: '{name}', + submitValue: '{isCreate}', + }, + name: 'name', + allowBlank: false, + }, + { + xtype: 'pveNodeSelector', + reference: 'nodeselector', + fieldLabel: gettext('Node'), + name: 'node', + cbind: { + disabled: '{hideNodeSelector}', + hidden: '{hideNodeSelector}', + }, + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + layout: 'fit', + cbind: { + disabled: '{hideMapping}', + hidden: '{hideMapping}', + }, + items: [ + { + xtype: 'textfield', + name: 'path', + reference: 'path', + value: '', + emptyText: gettext('/some/path'), + cbind: { + nodename: '{nodename}', + disabled: '{hideMapping}', + }, + allowBlank: false, + fieldLabel: gettext('Path'), + }, + ], + }, + ], + + columnB: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + layout: 'fit', + cbind: { + disabled: '{hideComment}', + hidden: '{hideComment}', + }, + items: [ + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Comment'), + submitValue: true, + name: 'description', + deleteEmpty: true, + }, + ], + }, + ], + }, + ], +}); diff --git a/www/manager6/window/PCIMapEdit.js b/www/manager6/window/PCIMapEdit.js new file mode 100644 index 0000000..625f4ad --- /dev/null +++ b/www/manager6/window/PCIMapEdit.js @@ -0,0 +1,305 @@ +Ext.define('PVE.window.PCIMapEditWindow', { + extend: 'Proxmox.window.Edit', + + mixins: ['Proxmox.Mixin.CBind'], + + width: 800, + + subject: gettext('PCI mapping'), + + onlineHelp: 'resource_mapping', + + method: 'POST', + + cbindData: function (initialConfig) { + let me = this; + me.isCreate = (!me.name || !me.nodename) && !me.entryOnly; + me.method = me.name ? 'PUT' : 'POST'; + me.hideMapping = !!me.entryOnly; + me.globalEdit = !me.name || me.entryOnly; + me.hideNodeSelector = me.nodename || me.entryOnly; + me.hideNode = !me.nodename || !me.hideNodeSelector; + return { + name: me.name, + nodename: me.nodename, + }; + }, + + submitUrl: function (_url, data) { + let me = this; + let name = me.method === 'PUT' ? me.name : ''; + return `/cluster/mapping/pci/${name}`; + }, + + controller: { + xclass: 'Ext.app.ViewController', + + onGetValues: function (values) { + let me = this; + let view = me.getView(); + if (view.method === 'POST') { + delete me.digest; + } + + if (values.iommugroup === -1) { + delete values.iommugroup; + } + + let nodename = values.node ?? view.nodename; + delete values.node; + if (me.originalMap) { + let otherMaps = PVE.Parser.filterPropertyStringList( + me.originalMap, + (e) => e.node !== nodename, + ); + if (otherMaps.length) { + values.map = values.map.concat(otherMaps); + } + } + + return values; + }, + + onSetValues: function (values) { + let me = this; + let view = me.getView(); + me.originalMap = [...values.map]; + let configuredNodes = []; + values.map = PVE.Parser.filterPropertyStringList(values.map, (e) => { + configuredNodes.push(e.node); + return e.node === view.nodename; + }); + + me.lookup('nodeselector').disallowedNodes = configuredNodes; + return values; + }, + + checkIommu: function (store, records, success) { + let me = this; + if (!success || !records.length) { + return; + } + me.lookup('iommu_warning').setVisible( + records.every((val) => val.data.iommugroup === -1), + ); + + let value = me.lookup('pciselector').getValue(); + me.checkIsolated(value); + }, + + checkIsolated: function (value) { + let me = this; + + let store = me.lookup('pciselector').getStore(); + + let isIsolated = function (entry) { + let isolated = true; + let parsed = PVE.Parser.parsePropertyString(entry); + parsed.iommugroup = parseInt(parsed.iommugroup, 10); + if (!parsed.iommugroup) { + return isolated; + } + store.each(({ data }) => { + let isSubDevice = data.id.startsWith(parsed.path); + if ( + data.iommugroup === parsed.iommugroup && + data.id !== parsed.path && + !isSubDevice + ) { + isolated = false; + return false; + } + return true; + }); + return isolated; + }; + + let showWarning = false; + if (Ext.isArray(value)) { + for (const entry of value) { + if (!isIsolated(entry)) { + showWarning = true; + break; + } + } + } else { + showWarning = isIsolated(value); + } + me.lookup('group_warning').setVisible(showWarning); + }, + + mdevChange: function (mdevField, value) { + this.lookup('pciselector').setMdev(value); + }, + + nodeChange: function (field, value) { + if (!field.isDisabled()) { + this.lookup('pciselector').setNodename(value); + } + }, + + pciChange: function (_field, value) { + let me = this; + me.lookup('multiple_warning').setVisible(Ext.isArray(value) && value.length > 1); + me.checkIsolated(value); + }, + + control: { + 'field[name=mdev]': { + change: 'mdevChange', + }, + pveNodeSelector: { + change: 'nodeChange', + }, + pveMultiPCISelector: { + change: 'pciChange', + }, + }, + }, + + items: [ + { + xtype: 'inputpanel', + onGetValues: function (values) { + return this.up('window').getController().onGetValues(values); + }, + + onSetValues: function (values) { + return this.up('window').getController().onSetValues(values); + }, + + columnT: [ + { + xtype: 'displayfield', + reference: 'iommu_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + value: gettext( + 'No IOMMU detected, please activate it. See Documentation for further information.', + ), + userCls: 'pmx-hint', + }, + { + xtype: 'displayfield', + reference: 'multiple_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + value: gettext( + 'When multiple devices are selected, the first free one will be chosen on guest start.', + ), + userCls: 'pmx-hint', + }, + { + xtype: 'displayfield', + reference: 'group_warning', + hidden: true, + columnWidth: 1, + padding: '0 0 10 0', + itemId: 'iommuwarning', + value: gettext( + 'A selected device is not in a separate IOMMU group, make sure this is intended.', + ), + userCls: 'pmx-hint', + }, + ], + + column1: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + labelWidth: 120, + cbind: { + editable: '{!name}', + value: '{name}', + submitValue: '{isCreate}', + }, + name: 'id', + allowBlank: false, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Mapping on Node'), + labelWidth: 120, + name: 'node', + cbind: { + value: '{nodename}', + disabled: '{hideNode}', + hidden: '{hideNode}', + }, + allowBlank: false, + }, + { + xtype: 'pveNodeSelector', + reference: 'nodeselector', + fieldLabel: gettext('Mapping on Node'), + labelWidth: 120, + name: 'node', + cbind: { + disabled: '{hideNodeSelector}', + hidden: '{hideNodeSelector}', + }, + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Use with Mediated Devices'), + labelWidth: 200, + reference: 'mdev', + name: 'mdev', + cbind: { + deleteEmpty: '{!isCreate}', + disabled: '{!globalEdit}', + }, + }, + { + xtype: 'proxmoxcheckbox', + fieldLabel: gettext('Live Migration Capable'), + labelWidth: 200, + boxLabel: ` ${gettext('Experimental')}`, + reference: 'live-migration-capable', + name: 'live-migration-capable', + cbind: { + deleteEmpty: '{!isCreate}', + disabled: '{!globalEdit}', + }, + }, + ], + + columnB: [ + { + xtype: 'pveMultiPCISelector', + fieldLabel: gettext('Device'), + labelWidth: 120, + height: 300, + reference: 'pciselector', + name: 'map', + cbind: { + nodename: '{nodename}', + disabled: '{hideMapping}', + hidden: '{hideMapping}', + }, + allowBlank: false, + onLoadCallBack: 'checkIommu', + margin: '0 0 10 0', + }, + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Comment'), + labelWidth: 120, + submitValue: true, + name: 'description', + cbind: { + deleteEmpty: '{!isCreate}', + disabled: '{!globalEdit}', + hidden: '{!globalEdit}', + }, + }, + ], + }, + ], +}); diff --git a/www/manager6/window/USBMapEdit.js b/www/manager6/window/USBMapEdit.js new file mode 100644 index 0000000..5c90ea3 --- /dev/null +++ b/www/manager6/window/USBMapEdit.js @@ -0,0 +1,252 @@ +Ext.define('PVE.window.USBMapEditWindow', { + extend: 'Proxmox.window.Edit', + + mixins: ['Proxmox.Mixin.CBind'], + + cbindData: function (initialConfig) { + let me = this; + me.isCreate = !me.name; + me.method = me.isCreate ? 'POST' : 'PUT'; + me.hideMapping = !!me.entryOnly; + me.hideComment = me.name && !me.entryOnly; + me.hideNodeSelector = me.nodename || me.entryOnly; + me.hideNode = !me.nodename || !me.hideNodeSelector; + return { + name: me.name, + nodename: me.nodename, + }; + }, + + submitUrl: function (_url, data) { + let me = this; + let name = me.isCreate ? '' : me.name; + return `/cluster/mapping/usb/${name}`; + }, + + title: gettext('Add USB mapping'), + + onlineHelp: 'resource_mapping', + + method: 'POST', + + controller: { + xclass: 'Ext.app.ViewController', + + onGetValues: function (values) { + let me = this; + let view = me.getView(); + values.node ??= view.nodename; + + let type = me.getView().down('radiofield').getGroupValue(); + let name = values.name; + let description = values.description; + delete values.description; + delete values.name; + + if (type === 'path') { + let usbsel = me.lookup(type); + let usbDev = usbsel + .getStore() + .findRecord('usbid', values[type], 0, false, true, true); + + if (!usbDev) { + return {}; + } + values.id = `${usbDev.data.vendid}:${usbDev.data.prodid}`; + } + + let map = []; + if (me.originalMap) { + map = PVE.Parser.filterPropertyStringList( + me.originalMap, + (e) => e.node !== values.node, + ); + } + if (values.id) { + map.push(PVE.Parser.printPropertyString(values)); + } + + values = { map }; + if (description) { + values.description = description; + } + + if (view.isCreate) { + values.id = name; + } + + return values; + }, + + onSetValues: function (values) { + let me = this; + let view = me.getView(); + me.originalMap = [...values.map]; + let configuredNodes = []; + PVE.Parser.filterPropertyStringList(values.map, (e) => { + configuredNodes.push(e.node); + if (e.node === view.nodename) { + values = e; + } + return false; + }); + + me.lookup('nodeselector').disallowedNodes = configuredNodes; + if (values.path) { + values.usb = 'path'; + } + + return values; + }, + + modeChange: function (field, value) { + let me = this; + let type = field.inputValue; + let usbsel = me.lookup(type); + usbsel.setDisabled(!value); + }, + + nodeChange: function (field, value) { + if (!field.isDisabled()) { + this.lookup('id').setNodename(value); + this.lookup('path').setNodename(value); + } + }, + + init: function (view) { + let _me = this; + + if (!view.nodename) { + //throw "no nodename given"; + } + }, + + control: { + radiofield: { + change: 'modeChange', + }, + pveNodeSelector: { + change: 'nodeChange', + }, + }, + }, + + items: [ + { + xtype: 'inputpanel', + onGetValues: function (values) { + return this.up('window').getController().onGetValues(values); + }, + + onSetValues: function (values) { + return this.up('window').getController().onSetValues(values); + }, + + column1: [ + { + xtype: 'pmxDisplayEditField', + fieldLabel: gettext('Name'), + cbind: { + editable: '{!name}', + value: '{name}', + submitValue: '{isCreate}', + }, + name: 'name', + allowBlank: false, + }, + { + xtype: 'displayfield', + fieldLabel: gettext('Mapping on Node'), + labelWidth: 120, + name: 'node', + cbind: { + value: '{nodename}', + disabled: '{hideNode}', + hidden: '{hideNode}', + }, + allowBlank: false, + }, + { + xtype: 'pveNodeSelector', + reference: 'nodeselector', + fieldLabel: gettext('Mapping on Node'), + labelWidth: 120, + name: 'node', + cbind: { + disabled: '{hideNodeSelector}', + hidden: '{hideNodeSelector}', + }, + allowBlank: false, + }, + ], + + column2: [ + { + xtype: 'fieldcontainer', + defaultType: 'radiofield', + layout: 'fit', + cbind: { + disabled: '{hideMapping}', + hidden: '{hideMapping}', + }, + items: [ + { + name: 'usb', + inputValue: 'id', + checked: true, + boxLabel: gettext('Use USB Vendor/Device ID'), + submitValue: false, + }, + { + xtype: 'pveUSBSelector', + type: 'device', + reference: 'id', + name: 'id', + cbind: { + nodename: '{nodename}', + disabled: '{hideMapping}', + }, + editable: true, + allowBlank: false, + fieldLabel: gettext('Choose Device'), + labelAlign: 'right', + }, + { + name: 'usb', + inputValue: 'path', + boxLabel: gettext('Use USB Port'), + submitValue: false, + }, + { + xtype: 'pveUSBSelector', + disabled: true, + name: 'path', + reference: 'path', + cbind: { + nodename: '{nodename}', + }, + editable: true, + type: 'port', + allowBlank: false, + fieldLabel: gettext('Choose Port'), + labelAlign: 'right', + }, + ], + }, + ], + + columnB: [ + { + xtype: 'proxmoxtextfield', + fieldLabel: gettext('Comment'), + submitValue: true, + name: 'description', + cbind: { + disabled: '{hideComment}', + hidden: '{hideComment}', + }, + }, + ], + }, + ], +});