[pve-devel] [RFC pve-manager master v1 10/12] ui: storage: add CustomBase.js

Max R. Carrara m.carrara at proxmox.com
Mon Sep 8 20:00:54 CEST 2025


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.carrara at 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





More information about the pve-devel mailing list