From: "Max R. Carrara" <m.carrara@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [RFC pve-manager master v1 10/12] ui: storage: add CustomBase.js
Date: Mon, 8 Sep 2025 20:00:54 +0200 [thread overview]
Message-ID: <20250908180058.530119-11-m.carrara@proxmox.com> (raw)
In-Reply-To: <20250908180058.530119-1-m.carrara@proxmox.com>
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@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
next prev parent reply other threads:[~2025-09-08 18:02 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-09-08 18:00 [pve-devel] [RFC pve-storage, pve-manager master v1 00/12] GUI Support for Custom Storage Plugins Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-storage master v1 01/12] plugin: meta: add package PVE::Storage::Plugin::Meta Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-storage master v1 02/12] api: Add 'plugins/storage' and 'plugins/storage/{plugin}' paths Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-storage master v1 03/12] plugin: meta: introduce 'short-name' Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-storage master v1 04/12] plugin: views: add package PVE::Storage::Plugin::Views Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-storage master v1 05/12] plugin: add new plugin API method `get_form_view()` Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-storage master v1 06/12] plugin: meta: add metadata regarding views in API Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-storage master v1 07/12] api: views: add paths regarding storage plugin views Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-storage master v1 08/12] plugin: zfspool: add 'short-name' and form view for ZFS pool plugin Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-manager master v1 09/12] api: handle path 'plugins/storage' through its package Max R. Carrara
2025-09-08 18:00 ` Max R. Carrara [this message]
2025-09-08 18:00 ` [pve-devel] [RFC pve-manager master v1 11/12] ui: storage: support custom storage plugins in Datacenter > Storage Max R. Carrara
2025-09-08 18:00 ` [pve-devel] [RFC pve-manager master v1 12/12] ui: storage: use `Ext.Msg.alert()` instead of throwing an exception Max R. Carrara
2025-09-08 19:23 ` [pve-devel] [RFC pve-storage, pve-manager master v1 00/12] GUI Support for Custom Storage Plugins Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250908180058.530119-11-m.carrara@proxmox.com \
--to=m.carrara@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox