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 <m.carr...@proxmox.com> --- 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