From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id ED85B1FF187 for ; Mon, 8 Sep 2025 20:02:17 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0C1AD18CE1; Mon, 8 Sep 2025 20:01:54 +0200 (CEST) From: "Max R. Carrara" To: pve-devel@lists.proxmox.com Date: Mon, 8 Sep 2025 20:00:54 +0200 Message-ID: <20250908180058.530119-11-m.carrara@proxmox.com> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250908180058.530119-1-m.carrara@proxmox.com> References: <20250908180058.530119-1-m.carrara@proxmox.com> MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1757354456584 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.088 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [RFC pve-manager master v1 10/12] ui: storage: add CustomBase.js X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" Add CustomBase.js, a copy of Base.js specifically for custom form views of storage plugin configs. While there is a large overlap between the files' contents, they are still kept separate for the purposes of this RFC. This makes it easier to differ between how custom storage plugins and inbuilt storage plugins are handled in the GUI at the moment, until this idea has been fleshed out more. The main UI building logic is in `PVE.storage.CustomInputPanel`. Right now, there are no custom fields or anything of the sort; the field's Ext.JS code is simply stitched together piece by piece depending on the form view definition provided. The fields for the 'storage', 'content', 'nodes' and 'disable' ('enable') are always included in every form view and cannot be disabled at the moment, as they exist in virtually every storage plugin. Signed-off-by: Max R. Carrara --- www/manager6/Makefile | 1 + www/manager6/storage/CustomBase.js | 402 +++++++++++++++++++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 www/manager6/storage/CustomBase.js diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 85f9268d..a329d36e 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -326,6 +326,7 @@ JSSRC= \ storage/ContentView.js \ storage/BackupView.js \ storage/Base.js \ + storage/CustomBase.js \ storage/Browser.js \ storage/CIFSEdit.js \ storage/CephFSEdit.js \ diff --git a/www/manager6/storage/CustomBase.js b/www/manager6/storage/CustomBase.js new file mode 100644 index 00000000..9ee2417c --- /dev/null +++ b/www/manager6/storage/CustomBase.js @@ -0,0 +1,402 @@ +Ext.define('PVE.panel.CustomStorageBase', { + extend: 'Proxmox.panel.InputPanel', + controller: 'storageEdit', + + type: '', + + onGetValues: function (values) { + let me = this; + + if (me.isCreate) { + values.type = me.type; + } else { + delete values.storage; + } + + values.disable = values.enable ? 0 : 1; + delete values.enable; + + return values; + }, + + initComponent: function () { + let me = this; + + me.column1.unshift( + { + xtype: me.isCreate ? 'textfield' : 'displayfield', + name: 'storage', + value: me.storageId || '', + fieldLabel: 'ID', + vtype: 'StorageId', + allowBlank: false, + }, + { + xtype: 'pveContentTypeSelector', + cts: me.metadataForPlugin.content.supported, + fieldLabel: gettext('Content'), + name: 'content', + value: me.metadataForPlugin.content.default, + multiSelect: true, + allowBlank: false, + }, + ); + + if (!me.column2) { + me.column2 = []; + } + + me.column2.unshift( + { + xtype: 'pveNodeSelector', + name: 'nodes', + reference: 'storageNodeRestriction', + disabled: me.storageId === 'local', + fieldLabel: gettext('Nodes'), + emptyText: gettext('All') + ' (' + gettext('No restrictions') + ')', + multiSelect: true, + autoSelect: false, + }, + { + xtype: 'proxmoxcheckbox', + name: 'enable', + checked: true, + uncheckedValue: 0, + fieldLabel: gettext('Enable'), + }, + ); + + me.callParent(); + }, +}); + +Ext.define('PVE.storage.CustomBaseEdit', { + extend: 'Proxmox.window.Edit', + + apiCallDone: function (success, response, options) { + let me = this; + if (typeof me.ipanel.apiCallDone === 'function') { + me.ipanel.apiCallDone(success, response, options); + } + }, + + initComponent: function () { + let me = this; + + me.isCreate = !me.storageId; + + if (me.isCreate) { + me.url = '/api2/extjs/storage'; + me.method = 'POST'; + } else { + me.url = '/api2/extjs/storage/' + me.storageId; + me.method = 'PUT'; + } + + me.ipanel = Ext.create(me.paneltype, { + title: gettext('General'), + type: me.type, + isCreate: me.isCreate, + storageId: me.storageId, + formView: me.formView, + metadataForPlugin: me.metadataForPlugin, + }); + + let subject = me.metadataForPlugin['short-name'] || PVE.Utils.format_storage_type(me.type); + + Ext.apply(me, { + subject: subject, + isAdd: true, + bodyPadding: 0, + items: { + xtype: 'tabpanel', + region: 'center', + layout: 'fit', + bodyPadding: 10, + items: [ + me.ipanel, + { + xtype: 'pveBackupJobPrunePanel', + title: gettext('Backup Retention'), + hasMaxProtected: true, + isCreate: me.isCreate, + keepAllDefaultForCreate: true, + showPBSHint: me.ipanel.isPBS, + fallbackHintHtml: gettext( + "Without any keep option, the node's vzdump.conf or `keep-all` is used as fallback for backup jobs", + ), + }, + ], + }, + }); + + if (me.ipanel.extraTabs) { + me.ipanel.extraTabs.forEach((panel) => { + panel.isCreate = me.isCreate; + me.items.items.push(panel); + }); + } + + me.callParent(); + + if (!me.canDoBackups) { + // cannot mask now, not fully rendered until activated + me.down('pmxPruneInputPanel').needMask = true; + } + + if (!me.isCreate) { + me.load({ + success: function (response, options) { + let values = response.result.data; + let ctypes = values.content || ''; + + values.content = ctypes.split(','); + + if (values.nodes) { + values.nodes = values.nodes.split(','); + } + values.enable = values.disable ? 0 : 1; + if (values['prune-backups']) { + let retention = PVE.Parser.parsePropertyString(values['prune-backups']); + delete values['prune-backups']; + Object.assign(values, retention); + } + + me.query('inputpanel').forEach((panel) => { + panel.setValues(values); + }); + }, + }); + } + }, +}); + +Ext.define('PVE.storage.CustomInputPanel', { + extend: 'PVE.panel.CustomStorageBase', + mixins: ['Proxmox.Mixin.CBind'], + + onlineHelp: '', + + buildFieldFromDefinition: function (me, fieldDef) { + let { property, label, attributes } = fieldDef; + + if (property in me.visitedStorageProperties) { + throw ( + `duplicate property '${property}' in form view` + + ` for custom storage plugin '${me.type}'` + ); + } + + me.visitedStorageProperties[property] = 1; + + let field = { + name: property, + fieldLabel: label, + cbind: {}, + }; + + switch (fieldDef['field-type']) { + case 'boolean': + field.xtype = 'proxmoxcheckbox'; + field.uncheckedValue = 0; + break; + + case 'integer': + field.xtype = 'proxmoxintegerfield'; + break; + + case 'number': + field.xtype = 'numberfield'; + break; + + case 'string': + switch (attributes['display-mode']) { + case 'text': + field.xtype = 'textfield'; + break; + case 'textarea': + field.xtype = 'textarea'; + break; + case 'password': + field.xtype = 'proxmoxtextfield'; + field.inputType = 'password'; + break; + default: + field.xtype = 'textfield'; + } + + break; + + case 'selection': + field.xtype = 'proxmoxKVComboBox'; + field.comboItems = attributes['selection-values'] || []; + field.autoSelect = true; + + if (me.isCreate) { + let firstPair = attributes['selection-values'][0]; + if (firstPair) { + field.value = firstPair[0]; + } + } + + switch (attributes['selection-mode']) { + case 'single': + field.multiSelect = false; + break; + case 'multi': + field.multiSelect = true; + break; + case 'default': + field.multiSelect = false; + } + + break; + + default: + field.xtype = 'displayfield'; + break; + } + + // **Common Attributes** + // required + if (attributes.required) { + field.allowBlank = false; + } + + // readonly + if (attributes.readonly) { + switch (fieldDef['field-type']) { + case 'boolean': + field.disabled = true; + break; + case 'integer': + field.xtype = 'displayfield'; + break; + case 'number': + field.xtype = 'displayfield'; + break; + case 'string': + field.xtype = 'displayfield'; + break; + case 'selection': + field.xtype = 'displayfield'; + break; + } + } + + // default + if (attributes.default && me.isCreate) { + switch (fieldDef['field-type']) { + case 'boolean': + field.value = Boolean(attributes.default); + field.checked = Boolean(attributes.default); + break; + + case 'integer': + field.value = Number(attributes.default); + break; + + case 'number': + field.value = Number(attributes.default); + break; + + case 'string': + field.value = attributes.default; + break; + + case 'selection': + switch (attributes['selection-mode']) { + case 'single': + field.value = attributes.default[0]; + break; + + case 'multi': + field.value = attributes.default; + break; + + default: + field.value = attributes.default[0]; + } + break; + } + } + + return field; + }, + + buildColumnFromDefinition: function (me, columnDef) { + return columnDef.fields.map((fieldDef) => me.buildFieldFromDefinition(me, fieldDef)); + }, + + initComponent: function () { + let me = this; + + // TODO: take schema version into account + + me.visitedStorageProperties = { + storage: 1, + content: 1, + notes: 1, + disable: 1, + enable: 1, + }; + + const viewDef = me.formView.definition.general; + const maxColumns = 2; + const maxAdvancedColumns = 2; + + let columns = viewDef.columns ?? []; + let columnBottom = viewDef['column-bottom']; + let advancedColumns = viewDef['columns-advanced'] ?? []; + let advancedColumnBottom = viewDef['column-advanced-bottom']; + + let columnCount = Math.min(columns.length, maxColumns); + + let advancedColumnCount = Math.min(advancedColumns.length, maxAdvancedColumns); + + try { + columns.slice(0, columnCount).map((columnDef, index) => { + let colName = 'column' + (index + 1); + + if (!me[colName]) { + me[colName] = []; + } + + me[colName] = me[colName].concat(me.buildColumnFromDefinition(me, columnDef)); + }); + + if (columnBottom) { + if (!me.columnB) { + me.columnB = []; + } + + me.columnB = me.columnB.concat(me.buildColumnFromDefinition(me, columnBottom)); + } + + advancedColumns.slice(0, advancedColumnCount).map((columnDef, index) => { + let colName = 'advancedColumn' + (index + 1); + + if (!me[colName]) { + me[colName] = []; + } + + me[colName] = me[colName].concat(me.buildColumnFromDefinition(me, columnDef)); + }); + + if (advancedColumnBottom) { + if (!me.advancedColumnB) { + me.advancedColumnB = []; + } + + me.advancedColumnB = me.advancedColumnB.concat( + me.buildColumnFromDefinition(me, advancedColumnBottom), + ); + } + } catch (error) { + Ext.Msg.alert(gettext('Error'), error); + return; + } + + me.callParent(); + }, +}); -- 2.47.2 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel