public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins
@ 2025-11-21 16:58 Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 1/10] api: plugins/storage: add initial routes and endpoints Max R. Carrara
                   ` (9 more replies)
  0 siblings, 10 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

GUI Support for Custom Storage Plugins - v2
===========================================

This is a complete refresh of my previous RFC that aims to be much less
greenfield-y and instead tries to reuse as much existing code as
possible while remaining as forward-compatible as possible.

Normally I would provide a more detailed changelog under each patch, but
since so much has changed since rfc-v1, I'll instead sum up the biggest
changes here:

- The API routes are changed to be a little more flexible—routes
  regarding views etc. are completely omitted. See patches #01 and #08
  for more information.

- The schemas of plugins are returned as part of their metadata. A
  "schema" for a storage plugin in this context is simply a hashmap
  consisting of the plugin's properties' JSON schemas. See patch #02 for
  a complete and detailed explanation.

- Existing UI code concerned with building the fields of ACME DNS
  challenge plugins is factored out, made more generic and also receives
  some additional features so that it can be used in different places.
  See patches #05 and #06 for details.

- Instead of making separate versions of all of the storage-related UI
  components (panels etc.), only one new input panel is added, while the
  remaining necessary functionality is integrated into the existing
  code. See patch #09 for further details.


The Result
----------

Instead of having to provide a separate "view definition", storage
entries for custom storage plugins can now be created and edited in the
UI *without* needing to provide any extra UI / layouting hints.

So, if one now installs something like the SSHFS example plugin
[sshfs-plugin], the resulting UI is about ~80% there in terms of look
and feel compared to the forms of built-in plugins:

- Default values are displayed as grey text inside fields
- Descriptions of properties are provided as little pop-up quips when
  hovering over a field
- "fixed" properties are automatically taken into account and made
  read-only when editing a storage entry
- "sensitive" properties are automatically treated as password fields
- Optional / non-optional properties are handled
- The min- / maxLength of strings and the min- / maximum of numeric
  fields is taken into account in the UI
- The "Backup Retention" tab is automatically masked / unmasked
  depending on whether the storage plugin supports backups or not

This is mostly everything that I managed to squeeze out of the existing
SectionConfig schemas / data.

Additionally, something that's neat is that the `title` key of every
property is now used as its field's label in the UI. That means we can
provide labels for all of our built-in properties while third-party
developers may provide theirs for their own properties. As a quick
example, this is what that would look like for the SSHFS plugin
[sshfs-plugin]:

  sub properties {
      return {
          'remote-path' => {
              description => "Path on the remote filesystem used for SSHFS. Must be absolute.",
              type => 'string',
              format => 'pve-storage-path',
              title => 'Remote Path',
          },
          'sshfs-private-key' => {
              description => "The private key to use for SSHFS.",
              type => 'string',
              title => 'Private Key',
          },
      };
  }


This becomes even more flexible for third-party devs once we switch over
to property isolation—something the code in this RFC should be 100%
forward-compatible with. Then they wouldn't be bound to our provided
labels.

In terms of overall looks, the remaining 20% consist of some polish on
the frontend for the most part, but honestly, that can be done once this
RFC becomes an actual series. I felt that this was in an adequate enough
state to publish for now—please let me know what you think if you give
this RFC a spin, I'd appreciate it!

Previous Versions
-----------------

rfc-v1: https://lore.proxmox.com/pve-devel/20250908180058.530119-1-m.carrara@proxmox.com/

References
----------

[sshfs-plugin]: https://git.proxmox.com/?p=pve-storage-plugin-examples.git;a=tree;f=plugin-sshfs;h=c7543808f7226209650d1b8b6e449392bc1f0d2d;hb=refs/heads/master

Summary of Changes
------------------

pve-storage:

Max R. Carrara (5):
  api: plugins/storage: add initial routes and endpoints
  api: plugins/storage/plugin: include schema in plugin metadata
  api: plugins/storage/plugin: mark sensitive properties in schema
  api: plugins/storage/plugin: factor plugin metadata code into helper
  api: plugins/storage/plugin: add plugins' 'content' to their metadata

 src/PVE/API2/Makefile                  |   1 +
 src/PVE/API2/Plugins/Makefile          |  18 +++
 src/PVE/API2/Plugins/Storage.pm        |  54 ++++++++
 src/PVE/API2/Plugins/Storage/Makefile  |  17 +++
 src/PVE/API2/Plugins/Storage/Plugin.pm | 163 +++++++++++++++++++++++++
 5 files changed, 253 insertions(+)
 create mode 100644 src/PVE/API2/Plugins/Makefile
 create mode 100644 src/PVE/API2/Plugins/Storage.pm
 create mode 100644 src/PVE/API2/Plugins/Storage/Makefile
 create mode 100644 src/PVE/API2/Plugins/Storage/Plugin.pm


proxmox-widget-toolkit:

Max R. Carrara (2):
  utils: introduce helper function getFieldDefFromPropertySchema
  acme: use helper to construct ExtJS fields from property schemas

 src/Utils.js                 | 106 +++++++++++++++++++++++++++++++++++
 src/window/ACMEPluginEdit.js |  42 +++++---------
 2 files changed, 119 insertions(+), 29 deletions(-)


pve-manager:

Max R. Carrara (3):
  api: add API routes 'plugins' and 'plugins/storage'
  ui: storage view: display error when no editor for storage type exists
  ui: storage: add basic UI integration for custom storage plugins

 PVE/API2.pm                        |   6 ++
 PVE/API2/Makefile                  |   1 +
 PVE/API2/Plugins.pm                |  61 +++++++++++++
 www/manager6/Makefile              |   1 +
 www/manager6/dc/StorageView.js     | 132 +++++++++++++++++++++--------
 www/manager6/storage/Base.js       |   1 +
 www/manager6/storage/CustomEdit.js | 110 ++++++++++++++++++++++++
 7 files changed, 278 insertions(+), 34 deletions(-)
 create mode 100644 PVE/API2/Plugins.pm
 create mode 100644 www/manager6/storage/CustomEdit.js

-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC pve-storage master v2 1/10] api: plugins/storage: add initial routes and endpoints
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 2/10] api: plugins/storage/plugin: include schema in plugin metadata Max R. Carrara
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

Add the following routes / endpoints:

* `GET plugins/storage`
  Subdir index--lists the direct child path components (so, just
  'plugin' currently).

* `GET plugins/storage/plugin`
  Lists all installed storage plugins and their metadata.
  Currently, this is just an array of objects, with each object
  containing the plugin's `type()` and Perl module path.

* `GET plugins/storage/plugin/{type}`
  Returns the metadata for a single plugin given by `{type}`.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Makefile                  |   1 +
 src/PVE/API2/Plugins/Makefile          |  18 +++++
 src/PVE/API2/Plugins/Storage.pm        |  54 +++++++++++++
 src/PVE/API2/Plugins/Storage/Makefile  |  17 ++++
 src/PVE/API2/Plugins/Storage/Plugin.pm | 104 +++++++++++++++++++++++++
 5 files changed, 194 insertions(+)
 create mode 100644 src/PVE/API2/Plugins/Makefile
 create mode 100644 src/PVE/API2/Plugins/Storage.pm
 create mode 100644 src/PVE/API2/Plugins/Storage/Makefile
 create mode 100644 src/PVE/API2/Plugins/Storage/Plugin.pm

diff --git a/src/PVE/API2/Makefile b/src/PVE/API2/Makefile
index fe316c5..01b7b28 100644
--- a/src/PVE/API2/Makefile
+++ b/src/PVE/API2/Makefile
@@ -5,3 +5,4 @@ install:
 	install -D -m 0644 Disks.pm ${DESTDIR}${PERLDIR}/PVE/API2/Disks.pm
 	make -C Storage install
 	make -C Disks install
+	make -C Plugins install
diff --git a/src/PVE/API2/Plugins/Makefile b/src/PVE/API2/Plugins/Makefile
new file mode 100644
index 0000000..f59e75b
--- /dev/null
+++ b/src/PVE/API2/Plugins/Makefile
@@ -0,0 +1,18 @@
+SOURCES = Storage.pm		\
+
+
+SUBDIRS = Storage		\
+
+
+INSTALL_PATH = ${DESTDIR}${PERLDIR}/PVE/API2/Plugins
+
+
+.PHONY: install
+install:
+	set -e && for SOURCE in ${SOURCES}; \
+		do install -D -m 0644 $$SOURCE ${INSTALL_PATH}/$$SOURCE; \
+	done
+	set -e && for SUBDIR in ${SUBDIRS}; \
+		do make -C $$SUBDIR install; \
+	done
+
diff --git a/src/PVE/API2/Plugins/Storage.pm b/src/PVE/API2/Plugins/Storage.pm
new file mode 100644
index 0000000..53b883f
--- /dev/null
+++ b/src/PVE/API2/Plugins/Storage.pm
@@ -0,0 +1,54 @@
+package PVE::API2::Plugins::Storage;
+
+use v5.36;
+
+use PVE::Storage;
+use PVE::Storage::Plugin;
+
+use PVE::API2::Plugins::Storage::Plugin;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+__PACKAGE__->register_method({
+    subclass => 'PVE::API2::Plugins::Storage::Plugin',
+    path => 'plugin',
+});
+
+# plugins/storage
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => 'Directory index.',
+    permissions => {
+        user => 'all',
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {},
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => "object",
+            additionalProperties => 0,
+            properties => {
+                subdir => {
+                    type => 'string',
+                },
+            },
+        },
+        links => [{ rel => 'child', href => "{subdir}" }],
+    },
+    code => sub($param) {
+        my $result = [
+            { subdir => 'plugin' },
+        ];
+
+        return $result;
+    },
+});
+
+1;
diff --git a/src/PVE/API2/Plugins/Storage/Makefile b/src/PVE/API2/Plugins/Storage/Makefile
new file mode 100644
index 0000000..06473a2
--- /dev/null
+++ b/src/PVE/API2/Plugins/Storage/Makefile
@@ -0,0 +1,17 @@
+SOURCES = Plugin.pm		\
+
+
+SUBDIRS =
+
+
+INSTALL_PATH = ${DESTDIR}${PERLDIR}/PVE/API2/Plugins/Storage
+
+.PHONY: install
+install:
+	set -e && for SOURCE in ${SOURCES}; \
+		do install -D -m 0644 $$SOURCE ${INSTALL_PATH}/$$SOURCE; \
+	done
+	set -e && for SUBDIR in ${SUBDIRS}; \
+		do make -C $$SUBDIR install; \
+	done
+
diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
new file mode 100644
index 0000000..fd0f734
--- /dev/null
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -0,0 +1,104 @@
+package PVE::API2::Plugins::Storage::Plugin;
+
+use v5.36;
+
+use HTTP::Status qw(:constants);
+
+use PVE::Exception qw(raise);
+use PVE::Storage;
+use PVE::Storage::Plugin;
+use PVE::Tools qw(extract_param);
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+my $ALL_PLUGIN_TYPES = PVE::Storage::Plugin->lookup_types();
+
+my $PLUGIN_METADATA_SCHEMA = {
+    type => 'object',
+    additionalProperties => 0,
+    properties => {
+        module => {
+            type => 'string',
+            optional => 0,
+        },
+        type => {
+            type => 'string',
+            optional => 0,
+            enum => $ALL_PLUGIN_TYPES,
+        },
+    },
+};
+
+# plugins/storage/plugin
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    description => 'List all available storage plugins and their metadata.',
+    permissions => {
+        user => 'all',
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {},
+    },
+    returns => {
+        type => 'array',
+        items => $PLUGIN_METADATA_SCHEMA,
+    },
+    code => sub($param) {
+        my $result = [];
+
+        for my $type ($ALL_PLUGIN_TYPES->@*) {
+            my $plugin = PVE::Storage::Plugin->lookup($type);
+
+            my $item = {
+                module => $plugin,
+                type => $type,
+            };
+
+            push($result->@*, $item);
+        }
+
+        return $result;
+    },
+});
+
+# plugins/storage/plugin/{type}
+
+__PACKAGE__->register_method({
+    name => 'info',
+    path => '{type}',
+    method => 'GET',
+    description => 'Fetch metadata of a storage plugin.',
+    permissions => {
+        user => 'all',
+    },
+    parameters => {
+        additionalProperties => 0,
+        properties => {
+            type => {
+                type => 'string',
+            },
+        },
+    },
+    returns => $PLUGIN_METADATA_SCHEMA,
+    code => sub($param) {
+        my $param_type = extract_param($param, 'type');
+
+        my $plugin = eval { PVE::Storage::Plugin->lookup($param_type) };
+        if ($@) {
+            raise("Plugin '$param_type' not found - $@", code => HTTP_NOT_FOUND);
+        }
+
+        my $result = {
+            module => $plugin,
+            type => $param_type,
+        };
+
+        return $result;
+    },
+});
+1;
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC pve-storage master v2 2/10] api: plugins/storage/plugin: include schema in plugin metadata
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 1/10] api: plugins/storage: add initial routes and endpoints Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 3/10] api: plugins/storage/plugin: mark sensitive properties in schema Max R. Carrara
                   ` (7 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

Return a simple schema describing the plugin as part of its metadata.

This schema is simply a hash consisting of a given plugin's
properties' schemas. Each property schema is obtained by calling the
`get_property_schema()` method of `PVE::SectionConfig` (which
`PVE::Storage::Plugin` inherits) for each property that a given plugin
uses in its `options()`. Additionally, each plugin's `options()` are
also taken into account, and the schemas are memoized.

This means that each returned schema is completely derived from the
given plugin's section config—the (globally) available properties and
the plugin's `options()`. At the same time, this should be adaptable
enough to include extra hints, such as whether a property is sensitive
or not, for example.

Deriving a schema for each plugin like this is more preferable over
exposing `createSchema()` and `updateSchema()` of
`PVE::Storage::Plugin` directly. In particular, these two schemas come
with some drawbacks when it comes to describing an *individual*
plugin's data:

- Neither schema contains any information on which properties are used
  by which plugins.

- It is not possible to determine whether a property is actually
  optional (or not) for a given plugin. If one plugin declares a
  property as optional somewhere, the property will be optional in the
  schema in most cases, despite being non-optional for other plugins.
  (This is generally true; skipping over a bunch of SectionConfig
  implementation details here for the sake of brevity).

- The same is true even more so for fixed properties. While the
  `updateSchema()` will oftentimes *not* contain fixed properties,
  meaning that you can compare it with `createSchema()` and figure out
  which properties *may* be fixed, this only holds if the property is
  fixed in *every* plugin's `options()`. As soon as a plugin declares
  a usually fixed property as non-fixed, the property is included in
  the `updateSchema()`.

- Even when switching to property isolation for
  `PVE::Storage::Plugin`, which properties are fixed / optional for
  individual plugins is still not consistently determinable for the
  previous two reasons.

This means that in addition to exposing `createSchema()` and
`updateSchema()`, we would also have to expose the `options()` of each
plugin, and then stitch all of that information together again just to
obtain the hash that the `get_schema_for_plugin()` helper being added
here returns.

Additionally, `get_property_schema()` takes property isolation into
account. More precisely, if one were to enable property isolation and
copy-paste the formerly global property definitions into the
`properties()` of each plugin where needed, the returned schema would
stay the same.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Plugins/Storage/Plugin.pm | 34 ++++++++++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
index fd0f734..573d3e4 100644
--- a/src/PVE/API2/Plugins/Storage/Plugin.pm
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -22,6 +22,10 @@ my $PLUGIN_METADATA_SCHEMA = {
             type => 'string',
             optional => 0,
         },
+        schema => {
+            type => 'object',
+            optional => 0,
+        },
         type => {
             type => 'string',
             optional => 0,
@@ -30,6 +34,34 @@ my $PLUGIN_METADATA_SCHEMA = {
     },
 };
 
+my $PLUGIN_SCHEMAS = {};
+
+my sub get_schema_for_plugin : prototype($) ($plugin) {
+    my $type = $plugin->type();
+
+    return $PLUGIN_SCHEMAS->{$type} if defined($PLUGIN_SCHEMAS->{$type});
+
+    my $options = $plugin->options();
+
+    my $schema = {};
+    $PLUGIN_SCHEMAS->{$type} = $schema;
+
+    for my $option (keys $options->%*) {
+        my $prop_schema = PVE::RESTHandler::api_dump_remove_refs(
+            PVE::Storage::Plugin->get_property_schema($type, $option));
+
+        # shallow copy
+        my $property = { $prop_schema->%* };
+        $schema->{$option} = $property;
+
+        for my $opt_key (keys $options->{$option}->%*) {
+            $property->{$opt_key} = $options->{$option}->{$opt_key};
+        }
+    }
+
+    return $schema;
+}
+
 # plugins/storage/plugin
 
 __PACKAGE__->register_method({
@@ -56,6 +88,7 @@ __PACKAGE__->register_method({
 
             my $item = {
                 module => $plugin,
+                schema => get_schema_for_plugin($plugin),
                 type => $type,
             };
 
@@ -95,6 +128,7 @@ __PACKAGE__->register_method({
 
         my $result = {
             module => $plugin,
+            schema => get_schema_for_plugin($plugin),
             type => $param_type,
         };
 
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC pve-storage master v2 3/10] api: plugins/storage/plugin: mark sensitive properties in schema
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 1/10] api: plugins/storage: add initial routes and endpoints Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 2/10] api: plugins/storage/plugin: include schema in plugin metadata Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 4/10] api: plugins/storage/plugin: factor plugin metadata code into helper Max R. Carrara
                   ` (6 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Plugins/Storage/Plugin.pm | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
index 573d3e4..757ae1e 100644
--- a/src/PVE/API2/Plugins/Storage/Plugin.pm
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -42,6 +42,7 @@ my sub get_schema_for_plugin : prototype($) ($plugin) {
     return $PLUGIN_SCHEMAS->{$type} if defined($PLUGIN_SCHEMAS->{$type});
 
     my $options = $plugin->options();
+    my $plugindata = $plugin->plugindata();
 
     my $schema = {};
     $PLUGIN_SCHEMAS->{$type} = $schema;
@@ -52,6 +53,8 @@ my sub get_schema_for_plugin : prototype($) ($plugin) {
 
         # shallow copy
         my $property = { $prop_schema->%* };
+        $property->{sensitive} = defined($plugindata->{'sensitive-properties'}->{$option}) || 0;
+
         $schema->{$option} = $property;
 
         for my $opt_key (keys $options->{$option}->%*) {
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC pve-storage master v2 4/10] api: plugins/storage/plugin: factor plugin metadata code into helper
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (2 preceding siblings ...)
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 3/10] api: plugins/storage/plugin: mark sensitive properties in schema Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 5/10] api: plugins/storage/plugin: add plugins' 'content' to their metadata Max R. Carrara
                   ` (5 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

in order to reduce the number of places that need to be touched when
adding more properties to the returned metadata.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Plugins/Storage/Plugin.pm | 35 +++++++++++---------------
 1 file changed, 15 insertions(+), 20 deletions(-)

diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
index 757ae1e..457c070 100644
--- a/src/PVE/API2/Plugins/Storage/Plugin.pm
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -65,6 +65,19 @@ my sub get_schema_for_plugin : prototype($) ($plugin) {
     return $schema;
 }
 
+my sub build_plugin_metadata : prototype($) ($type) {
+    my $plugin = eval { PVE::Storage::Plugin->lookup($type) };
+    if ($@) {
+        raise("Plugin '$type' not found - $@", code => HTTP_NOT_FOUND);
+    }
+
+    return {
+        module => $plugin,
+        schema => get_schema_for_plugin($plugin),
+        type => $type,
+    };
+}
+
 # plugins/storage/plugin
 
 __PACKAGE__->register_method({
@@ -87,14 +100,7 @@ __PACKAGE__->register_method({
         my $result = [];
 
         for my $type ($ALL_PLUGIN_TYPES->@*) {
-            my $plugin = PVE::Storage::Plugin->lookup($type);
-
-            my $item = {
-                module => $plugin,
-                schema => get_schema_for_plugin($plugin),
-                type => $type,
-            };
-
+            my $item = build_plugin_metadata($type);
             push($result->@*, $item);
         }
 
@@ -124,18 +130,7 @@ __PACKAGE__->register_method({
     code => sub($param) {
         my $param_type = extract_param($param, 'type');
 
-        my $plugin = eval { PVE::Storage::Plugin->lookup($param_type) };
-        if ($@) {
-            raise("Plugin '$param_type' not found - $@", code => HTTP_NOT_FOUND);
-        }
-
-        my $result = {
-            module => $plugin,
-            schema => get_schema_for_plugin($plugin),
-            type => $param_type,
-        };
-
-        return $result;
+        return build_plugin_metadata($param_type);
     },
 });
 1;
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC pve-storage master v2 5/10] api: plugins/storage/plugin: add plugins' 'content' to their metadata
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (3 preceding siblings ...)
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 4/10] api: plugins/storage/plugin: factor plugin metadata code into helper Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC proxmox-widget-toolkit master v2 6/10] utils: introduce helper function getFieldDefFromPropertySchema Max R. Carrara
                   ` (4 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/PVE/API2/Plugins/Storage/Plugin.pm | 27 ++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/src/PVE/API2/Plugins/Storage/Plugin.pm b/src/PVE/API2/Plugins/Storage/Plugin.pm
index 457c070..6db9ffc 100644
--- a/src/PVE/API2/Plugins/Storage/Plugin.pm
+++ b/src/PVE/API2/Plugins/Storage/Plugin.pm
@@ -18,6 +18,27 @@ my $PLUGIN_METADATA_SCHEMA = {
     type => 'object',
     additionalProperties => 0,
     properties => {
+        content => {
+            type => 'object',
+            optional => 0,
+            properties => {
+                supported => {
+                    type => 'array',
+                    optional => 0,
+                    items => {
+                        type => 'string',
+                    },
+                },
+                default => {
+                    type => 'array',
+                    optional => 0,
+                    items => {
+                        type => 'string',
+                    },
+                },
+            },
+            additionalProperties => 0,
+        },
         module => {
             type => 'string',
             optional => 0,
@@ -71,7 +92,13 @@ my sub build_plugin_metadata : prototype($) ($type) {
         raise("Plugin '$type' not found - $@", code => HTTP_NOT_FOUND);
     }
 
+    my $plugindata = $plugin->plugindata();
+
     return {
+        content => {
+            supported => [sort keys $plugindata->{content}->[0]->%*],
+            default => [sort keys $plugindata->{content}->[1]->%*],
+        },
         module => $plugin,
         schema => get_schema_for_plugin($plugin),
         type => $type,
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC proxmox-widget-toolkit master v2 6/10] utils: introduce helper function getFieldDefFromPropertySchema
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (4 preceding siblings ...)
  2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 5/10] api: plugins/storage/plugin: add plugins' 'content' to their metadata Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC proxmox-widget-toolkit master v2 7/10] acme: use helper to construct ExtJS fields from property schemas Max R. Carrara
                   ` (3 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

This helper takes the JSONSchema of a single property and transforms
it into an object representing an ExtJS field.

Currently, four types for properties are supported: string, integer,
number and boolean.

Additionally, the helper also takes "extra attributes" and a "context"
which allows for more fine-grained control over how the ExtJS field is
actually constructed and also makes it possible to express different
UI-specific behaviors that cannot be modeled via JSONSchema (for
example, whether a field's contents are sensitive).

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/Utils.js | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 106 insertions(+)

diff --git a/src/Utils.js b/src/Utils.js
index 5457ffa..a9bcb56 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -1571,6 +1571,112 @@ Ext.define('Proxmox.Utils', {
             }
             hiddenElement.click();
         },
+
+        /**
+         * Takes a JSONSchema of a single property and transforms it into an
+         * object representing an ExtJS field.
+         *
+         * @param {string} fieldName - The value for the `name` property of the resulting object.
+         * @param {Object} propSchema - The JSONSchema of a property.
+         * @param {Object} extraAttributes - Additional attributes describing the
+         * field that are not or cannot be represented by the given JSONSchema.
+         * Note that none of these attributes represent any specific ExtJS property.
+         * @param {Boolean} extraAttributes.readonly - Whether the field shall be read-only.
+         * "Read-only" here means that the field's `xtype` will be set to
+         * `'displayfield'`, regardless of what data type the field will
+         * display. Additionally, checkboxes (booleans) will be displayed as
+         * "Yes" or "No" depending on their value (locale-aware).
+         * @param {Boolean} extraAttributes.sensitive - Whether the field
+         * contains sensitive data, such as a password or similar.
+         * @param {Object} context - The context within which the field is being used.
+         * @param {Boolean} context.isCreate - Whether the field will be used during creation.
+         */
+        getFieldDefFromPropertySchema: function (fieldName, propSchema, extraAttributes, context) {
+            const fieldPropsByType = {
+                string: {
+                    xtype: 'proxmoxtextfield',
+                    minLength: propSchema.minLength,
+                    maxLength: propSchema.maxLength,
+                },
+                integer: {
+                    xtype: 'proxmoxintegerfield',
+                    minValue: propSchema.minimum,
+                    maxValue: propSchema.maximum,
+                },
+                number: {
+                    xtype: 'numberfield',
+                    minValue: propSchema.minimum,
+                    maxValue: propSchema.maximum,
+                },
+                boolean: {
+                    xtype: 'proxmoxcheckbox',
+                    uncheckedValue: 0,
+                },
+            };
+
+            const commonFieldProps = {
+                name: fieldName,
+                emptyText: propSchema.default || '',
+                allowBlank: propSchema.optional,
+            };
+
+            let fieldProps = fieldPropsByType[propSchema.type];
+            if (fieldProps === undefined) {
+                console.warn(`Unhandled property type '${propSchema.type}'`);
+                fieldProps = fieldPropsByType.string;
+            }
+
+            let field = {
+                ...fieldProps,
+                ...commonFieldProps,
+            };
+
+            if (propSchema.title) {
+                field.fieldLabel = Ext.htmlEncode(propSchema.title);
+            }
+
+            if (propSchema.description) {
+                field.autoEl = {
+                    tag: 'div',
+                    'data-qtip': Ext.htmlEncode(Ext.htmlEncode(propSchema.description)),
+                };
+            }
+
+            if (context.isCreate && propSchema.default !== undefined) {
+                if (propSchema.type === 'boolean') {
+                    field.checked = propSchema.default;
+                } else {
+                    field.value = propSchema.default;
+                }
+            }
+
+            if (extraAttributes.sensitive) {
+                field.xtype = 'proxmoxtextfield';
+                field.inputType = 'password';
+
+                if (context.isCreate) {
+                    field.value = '';
+                    field.emptyText = Proxmox.Utils.NoneText;
+                } else {
+                    field.emptyText = gettext('Unchanged');
+                }
+            }
+
+            if (extraAttributes.readonly) {
+                field.xtype = 'displayfield';
+
+                if (propSchema.type === 'boolean') {
+                    field.renderer = Proxmox.Utils.format_boolean;
+                }
+
+                if (extraAttributes.sensitive) {
+                    field.value = '*********';
+                    field.emptyText = '';
+                }
+            }
+
+            return field;
+        },
     },
 
     singleton: true,
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC proxmox-widget-toolkit master v2 7/10] acme: use helper to construct ExtJS fields from property schemas
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (5 preceding siblings ...)
  2025-11-21 16:58 ` [pve-devel] [RFC proxmox-widget-toolkit master v2 6/10] utils: introduce helper function getFieldDefFromPropertySchema Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 08/10] api: add API routes 'plugins' and 'plugins/storage' Max R. Carrara
                   ` (2 subsequent siblings)
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

Use the new `getFieldDefFromPropertySchema()` helper function to
simplify some of the logic in `ACMEPluginEdit.js`.

This results in no noticeable difference when setting up ACME challenge
plugins, except for the fact that fields not marked as optional are
now in fact not optional in the UI.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 src/window/ACMEPluginEdit.js | 42 +++++++++++-------------------------
 1 file changed, 13 insertions(+), 29 deletions(-)

diff --git a/src/window/ACMEPluginEdit.js b/src/window/ACMEPluginEdit.js
index 900923b..d8591ae 100644
--- a/src/window/ACMEPluginEdit.js
+++ b/src/window/ACMEPluginEdit.js
@@ -69,45 +69,29 @@ Ext.define('Proxmox.window.ACMEPluginEdit', {
                 for (const [name, definition] of Object.entries(schema.fields).sort((a, b) =>
                     a[0].localeCompare(b[0]),
                 )) {
-                    let xtype;
-                    switch (definition.type) {
-                        case 'string':
-                            xtype = 'proxmoxtextfield';
-                            break;
-                        case 'integer':
-                            xtype = 'proxmoxintegerfield';
-                            break;
-                        case 'number':
-                            xtype = 'numberfield';
-                            break;
-                        default:
-                            console.warn(`unknown type '${definition.type}'`);
-                            xtype = 'proxmoxtextfield';
-                            break;
-                    }
+                    let fieldName = `custom_${name}`;
+                    let context = { isCreate: me.isCreate };
+
+                    let fieldDef = Proxmox.Utils.getFieldDefFromPropertySchema(
+                        fieldName,
+                        definition,
+                        {},
+                        context,
+                    );
 
                     let label = name;
                     if (typeof definition.name === 'string') {
                         label = definition.name;
                     }
 
-                    let field = Ext.create({
-                        xtype,
-                        name: `custom_${name}`,
+                    let extraProps = {
                         fieldLabel: Ext.htmlEncode(label),
                         width: '100%',
                         labelWidth: 150,
                         labelSeparator: '=',
-                        emptyText: definition.default || '',
-                        autoEl: definition.description
-                            ? {
-                                  tag: 'div',
-                                  'data-qtip': Ext.htmlEncode(
-                                      Ext.htmlEncode(definition.description),
-                                  ),
-                              }
-                            : undefined,
-                    });
+                    };
+
+                    let field = Ext.create({ ...fieldDef, ...extraProps });
 
                     me.createdFields[name] = field;
                     container.add(field);
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC pve-manager master v2 08/10] api: add API routes 'plugins' and 'plugins/storage'
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (6 preceding siblings ...)
  2025-11-21 16:58 ` [pve-devel] [RFC proxmox-widget-toolkit master v2 7/10] acme: use helper to construct ExtJS fields from property schemas Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 09/10] ui: storage view: display error when no editor for storage type exists Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 10/10] ui: storage: add basic UI integration for custom storage plugins Max R. Carrara
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

Add the `plugins` route to our API and handle it via its own module,
`API2/Plugins.pm`. Implement a GET endpoint for this route, which just
lists its subpaths.

Additionally, add the `plugins/storage` path to the API and let all
corresponding endpoints be handled by the
`PVE::API2::Plugins::Storage` module, which is part of the
`libpve-storage-perl` package.

This means that the following routes are now available:
* plugins
* plugins/storage
* plugins/storage/plugin
* plugins/storage/plugin/{type}

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 PVE/API2.pm         |  6 +++++
 PVE/API2/Makefile   |  1 +
 PVE/API2/Plugins.pm | 61 +++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 68 insertions(+)
 create mode 100644 PVE/API2/Plugins.pm

diff --git a/PVE/API2.pm b/PVE/API2.pm
index 0c7e4654..af1cf38b 100644
--- a/PVE/API2.pm
+++ b/PVE/API2.pm
@@ -17,6 +17,7 @@ use PVE::API2::Nodes;
 use PVE::API2::Pool;
 use PVE::API2::AccessControl;
 use PVE::API2::Storage::Config;
+use PVE::API2::Plugins;
 
 __PACKAGE__->register_method({
     subclass => "PVE::API2::Cluster",
@@ -43,6 +44,11 @@ __PACKAGE__->register_method({
     path => 'pools',
 });
 
+__PACKAGE__->register_method({
+    subclass => "PVE::API2::Plugins",
+    path => 'plugins',
+});
+
 __PACKAGE__->register_method({
     name => 'index',
     path => '',
diff --git a/PVE/API2/Makefile b/PVE/API2/Makefile
index 97f1cc20..97910009 100644
--- a/PVE/API2/Makefile
+++ b/PVE/API2/Makefile
@@ -17,6 +17,7 @@ PERLSOURCE = 			\
 	Network.pm		\
 	NodeConfig.pm		\
 	Nodes.pm		\
+	Plugins.pm		\
 	Pool.pm			\
 	Replication.pm		\
 	ReplicationConfig.pm	\
diff --git a/PVE/API2/Plugins.pm b/PVE/API2/Plugins.pm
new file mode 100644
index 00000000..beb606e6
--- /dev/null
+++ b/PVE/API2/Plugins.pm
@@ -0,0 +1,61 @@
+package PVE::API2::Plugins;
+
+use v5.36;
+
+use PVE::RESTHandler;
+use base qw(PVE::RESTHandler);
+
+use PVE::API2::Plugins::Storage;
+
+__PACKAGE__->register_method({
+    subclass => "PVE::API2::Plugins::Storage",
+    path => 'storage',
+});
+
+__PACKAGE__->register_method({
+    name => 'index',
+    path => '',
+    method => 'GET',
+    permissions => {
+        user => 'all',
+    },
+    description => "Directory index.",
+    parameters => {
+        additionalProperties => 0,
+        properties => {},
+    },
+    returns => {
+        type => 'array',
+        items => {
+            type => 'object',
+            properties => {
+                subdir => {
+                    type => 'string',
+                },
+            },
+        },
+        links => [
+            {
+                rel => 'child',
+                href => "{subdir}",
+            },
+        ],
+    },
+    code => sub($param) {
+        my $result = [];
+
+        my $attrs = PVE::API2::Plugins->method_attributes();
+
+        for my $info ($attrs->@*) {
+            next if !$info->{subclass};
+
+            my $subpath = $info->{match_re}->[0];
+
+            push $result->@*, { subdir => $subpath };
+        }
+
+        return $result;
+    },
+});
+
+1;
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC pve-manager master v2 09/10] ui: storage view: display error when no editor for storage type exists
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (7 preceding siblings ...)
  2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 08/10] api: add API routes 'plugins' and 'plugins/storage' Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 10/10] ui: storage: add basic UI integration for custom storage plugins Max R. Carrara
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

... instead of `throw`ing an exception which gets swallowed by ExtJS
and never displayed to the user.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 www/manager6/dc/StorageView.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/www/manager6/dc/StorageView.js b/www/manager6/dc/StorageView.js
index e4c6f07d..bcc02ed5 100644
--- a/www/manager6/dc/StorageView.js
+++ b/www/manager6/dc/StorageView.js
@@ -13,7 +13,8 @@ Ext.define(
         createStorageEditWindow: function (type, sid) {
             let schema = PVE.Utils.storageSchema[type];
             if (!schema || !schema.ipanel) {
-                throw 'no editor registered for storage type: ' + type;
+                Ext.Msg.alert(gettext('Error'), `No editor registered for storage type '${type}'`);
+                return;
             }
 
             Ext.create('PVE.storage.BaseEdit', {
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [pve-devel] [RFC pve-manager master v2 10/10] ui: storage: add basic UI integration for custom storage plugins
  2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
                   ` (8 preceding siblings ...)
  2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 09/10] ui: storage view: display error when no editor for storage type exists Max R. Carrara
@ 2025-11-21 16:58 ` Max R. Carrara
  9 siblings, 0 replies; 11+ messages in thread
From: Max R. Carrara @ 2025-11-21 16:58 UTC (permalink / raw)
  To: pve-devel

This commit adds a basic / rudimentary UI integration for custom
storage plugins.

Do this by issuing a request to the new `GET plugins/storage/plugin`
endpoint and checking whether a plugin is custom or not via its Perl
module path. When a user then adds or edits a storage config entry
belonging to a custom storage plugin, the new
`PVE.storage.CustomInputPanel` opens and builds the form's view from
the schemas of the plugin's properties.

In other words, the UI is built completely from the information
provided by the plugin's SectionConfig schema.

It is worth noting that the "Add" dropdown menu button's items are
added to the menu on the fly once the request to
`plugins/storage/plugin` succeeds. For the short moment that the
request is being awaited, the "Add" button is disabled.

This is not noticeable at all for regular (i.e. decently fast)
connections. Users with (very) slow connections will however notice
that the button stays disabled until the request succeeded. If that
were not the case, the dropdown menu of the "Add" button would not
even show up when clicked. Therefore keep the button disabled (greyed
out, unclickable) for the little time that the user cannot do anything
with it anyway.

Signed-off-by: Max R. Carrara <m.carrara@proxmox.com>
---
 www/manager6/Makefile              |   1 +
 www/manager6/dc/StorageView.js     | 131 +++++++++++++++++++++--------
 www/manager6/storage/Base.js       |   1 +
 www/manager6/storage/CustomEdit.js | 110 ++++++++++++++++++++++++
 4 files changed, 209 insertions(+), 34 deletions(-)
 create mode 100644 www/manager6/storage/CustomEdit.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 4558d53e..85401b4f 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -339,6 +339,7 @@ JSSRC= 							\
 	storage/Browser.js				\
 	storage/CIFSEdit.js				\
 	storage/CephFSEdit.js				\
+	storage/CustomEdit.js				\
 	storage/DirEdit.js				\
 	storage/ImageView.js				\
 	storage/IScsiEdit.js				\
diff --git a/www/manager6/dc/StorageView.js b/www/manager6/dc/StorageView.js
index bcc02ed5..7bcb5637 100644
--- a/www/manager6/dc/StorageView.js
+++ b/www/manager6/dc/StorageView.js
@@ -11,20 +11,47 @@ Ext.define(
         stateId: 'grid-dc-storage',
 
         createStorageEditWindow: function (type, sid) {
-            let schema = PVE.Utils.storageSchema[type];
-            if (!schema || !schema.ipanel) {
-                Ext.Msg.alert(gettext('Error'), `No editor registered for storage type '${type}'`);
+            let me = this;
+
+            const metadata = me.pluginMetadata[type];
+
+            // Should never happen, but still handle it here just in case
+            if (!metadata) {
+                Ext.Msg.alert(gettext('Error'), `Plugin '${type}' has no metadata`);
                 return;
             }
 
+            let isCustom = metadata.module.startsWith('PVE::Storage::Custom');
+
+            let paneltype;
+            let canDoBackups;
+
+            if (isCustom) {
+                paneltype = 'PVE.storage.CustomInputPanel';
+                canDoBackups = metadata.content.supported.includes('backup');
+            } else {
+                let schema = PVE.Utils.storageSchema[type];
+                if (!schema || !schema.ipanel) {
+                    Ext.Msg.alert(
+                        gettext('Error'),
+                        `No editor registered for storage type '${type}'`,
+                    );
+                    return;
+                }
+
+                paneltype = 'PVE.storage.' + schema.ipanel;
+                canDoBackups = schema.backups;
+            }
+
             Ext.create('PVE.storage.BaseEdit', {
-                paneltype: 'PVE.storage.' + schema.ipanel,
+                paneltype: paneltype,
                 type: type,
                 storageId: sid,
-                canDoBackups: schema.backups,
+                canDoBackups: canDoBackups,
+                metadata: metadata,
                 autoShow: true,
                 listeners: {
-                    destroy: this.reloadStore,
+                    destroy: me.reloadStore,
                 },
             });
         },
@@ -46,6 +73,69 @@ Ext.define(
 
             let sm = Ext.create('Ext.selection.RowModel', {});
 
+            me.pluginMetadata = {};
+
+            let menuButtonAdd = new Ext.menu.Menu({
+                items: [],
+            });
+
+            let addBtn = new Ext.Button({
+                menu: menuButtonAdd,
+                text: gettext('Add'),
+                disabled: true,
+            });
+
+            let pushBuiltinPluginsToMenu = function () {
+                for (const [type, storage] of Object.entries(PVE.Utils.storageSchema)) {
+                    if (storage.hideAdd) {
+                        continue;
+                    }
+
+                    menuButtonAdd.add({
+                        text: PVE.Utils.format_storage_type(type),
+                        iconCls: 'fa fa-fw fa-' + storage.faIcon,
+                        handler: () => me.createStorageEditWindow(type),
+                    });
+                }
+            };
+
+            let pushCustomPluginsToMenu = function () {
+                for (const type in me.pluginMetadata) {
+                    if (!Object.hasOwn(me.pluginMetadata, type)) {
+                        continue;
+                    }
+
+                    const metadata = me.pluginMetadata[type];
+                    let isCustom = metadata.module.startsWith('PVE::Storage::Custom');
+
+                    if (isCustom) {
+                        menuButtonAdd.add({
+                            text: PVE.Utils.format_storage_type(type),
+                            iconCls: 'fa fa-fw fa-folder',
+                            handler: () => me.createStorageEditWindow(type),
+                        });
+                    }
+                }
+            };
+
+            Proxmox.Utils.API2Request({
+                url: `/api2/extjs/plugins/storage/plugin`,
+                method: 'GET',
+                success: function ({ result: { data } }) {
+                    data.forEach((metadata) => {
+                        me.pluginMetadata[metadata.type] = metadata;
+                    });
+
+                    pushBuiltinPluginsToMenu();
+                    pushCustomPluginsToMenu();
+
+                    addBtn.setDisabled(false);
+                },
+                failure: function ({ htmlStatus }) {
+                    Ext.Msg.alert('Error', htmlStatus);
+                },
+            });
+
             let run_editor = function () {
                 let rec = sm.getSelection()[0];
                 if (!rec) {
@@ -67,24 +157,6 @@ Ext.define(
                 callback: () => store.load(),
             });
 
-            // else we cannot dynamically generate the add menu handlers
-            let addHandleGenerator = function (type) {
-                return function () {
-                    me.createStorageEditWindow(type);
-                };
-            };
-            let addMenuItems = [];
-            for (const [type, storage] of Object.entries(PVE.Utils.storageSchema)) {
-                if (storage.hideAdd) {
-                    continue;
-                }
-                addMenuItems.push({
-                    text: PVE.Utils.format_storage_type(type),
-                    iconCls: 'fa fa-fw fa-' + storage.faIcon,
-                    handler: addHandleGenerator(type),
-                });
-            }
-
             Ext.apply(me, {
                 store: store,
                 reloadStore: () => store.load(),
@@ -92,16 +164,7 @@ Ext.define(
                 viewConfig: {
                     trackOver: false,
                 },
-                tbar: [
-                    {
-                        text: gettext('Add'),
-                        menu: new Ext.menu.Menu({
-                            items: addMenuItems,
-                        }),
-                    },
-                    remove_btn,
-                    edit_btn,
-                ],
+                tbar: [addBtn, remove_btn, edit_btn],
                 columns: [
                     {
                         header: 'ID',
diff --git a/www/manager6/storage/Base.js b/www/manager6/storage/Base.js
index cf89ef6d..43fc70d6 100644
--- a/www/manager6/storage/Base.js
+++ b/www/manager6/storage/Base.js
@@ -138,6 +138,7 @@ Ext.define('PVE.storage.BaseEdit', {
             type: me.type,
             isCreate: me.isCreate,
             storageId: me.storageId,
+            metadata: me.metadata,
         });
 
         Ext.apply(me, {
diff --git a/www/manager6/storage/CustomEdit.js b/www/manager6/storage/CustomEdit.js
new file mode 100644
index 00000000..a7e7c7e0
--- /dev/null
+++ b/www/manager6/storage/CustomEdit.js
@@ -0,0 +1,110 @@
+Ext.define('PVE.storage.CustomInputPanel', {
+    extend: 'PVE.panel.StorageBase',
+
+    buildFormFieldFromProperty: function (propertyName) {
+        let me = this;
+        let schema = me.metadata.schema;
+
+        let property = schema[propertyName];
+
+        if (!property) {
+            console.warn(
+                `Tried to create field for unknown property '${propertyName}'` +
+                    ` for storage type '${me.type}'`,
+            );
+            return;
+        }
+
+        let fieldName = propertyName;
+        let extraAttributes = {
+            readonly: property.fixed && !me.isCreate,
+            sensitive: property.sensitive,
+        };
+        let context = { isCreate: me.isCreate };
+
+        let fieldDef = Proxmox.Utils.getFieldDefFromPropertySchema(
+            fieldName,
+            property,
+            extraAttributes,
+            context,
+        );
+
+        if (!fieldDef.fieldLabel) {
+            fieldDef.fieldLabel = Ext.htmlEncode(propertyName);
+        }
+
+        return fieldDef;
+    },
+
+    addWidget: function (widget) {
+        let me = this;
+
+        me.column1 = me.column1 || [];
+        me.column2 = me.column2 || [];
+
+        if (me.column2.length >= me.column1.length) {
+            me.column1.push(widget);
+        } else {
+            me.column2.push(widget);
+        }
+    },
+
+    initComponent: function () {
+        let me = this;
+        let schema = me.metadata.schema;
+
+        me.column1 = me.column1 || [];
+        me.column2 = me.column2 || [];
+
+        const reservedFields = new Set([
+            // automatically added in PVE.panel.StorageBase
+            'storage',
+            'nodes',
+            'disable',
+
+            // handled separately for consistency
+            'content',
+            'shared',
+
+            // not an actual property, but used by the UI as an inverse of the 'disable' property
+            'enable',
+
+            // handled by the "Backup Retention" panel
+            'prune-backups',
+            'max-protected-backups',
+        ]);
+
+        // Add the field for the 'content' property first for consistency's sake
+        let propertyContent = schema.content;
+        if (propertyContent) {
+            let fieldDefContent = {
+                xtype: 'pveContentTypeSelector',
+                cts: me.metadata.content.supported,
+                fieldLabel: gettext('Content'),
+                name: 'content',
+                value: me.metadata.content.default,
+                multiSelect: true,
+                allowBlank: false,
+            };
+
+            me.column1.push(fieldDefContent);
+        }
+
+        let propertyShared = schema.shared;
+        if (propertyShared) {
+            let fieldDefShared = me.buildFormFieldFromProperty('shared');
+            me.column1.push(fieldDefShared);
+        }
+
+        for (const propertyName of Object.keys(schema).sort()) {
+            if (reservedFields.has(propertyName)) {
+                continue;
+            }
+
+            let fieldDef = me.buildFormFieldFromProperty(propertyName);
+            me.addWidget(fieldDef);
+        }
+
+        me.callParent();
+    },
+});
-- 
2.47.3



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2025-11-21 17:00 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-11-21 16:58 [pve-devel] [RFC pve-storage/proxmox-widget-toolkit/pve-manager master v2 00/10] GUI Support for Custom Storage Plugins Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 1/10] api: plugins/storage: add initial routes and endpoints Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 2/10] api: plugins/storage/plugin: include schema in plugin metadata Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 3/10] api: plugins/storage/plugin: mark sensitive properties in schema Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 4/10] api: plugins/storage/plugin: factor plugin metadata code into helper Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC pve-storage master v2 5/10] api: plugins/storage/plugin: add plugins' 'content' to their metadata Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC proxmox-widget-toolkit master v2 6/10] utils: introduce helper function getFieldDefFromPropertySchema Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC proxmox-widget-toolkit master v2 7/10] acme: use helper to construct ExtJS fields from property schemas Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 08/10] api: add API routes 'plugins' and 'plugins/storage' Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 09/10] ui: storage view: display error when no editor for storage type exists Max R. Carrara
2025-11-21 16:58 ` [pve-devel] [RFC pve-manager master v2 10/10] ui: storage: add basic UI integration for custom storage plugins Max R. Carrara

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal