[pve-devel] [RFC pve-storage master v1 04/12] plugin: views: add package PVE::Storage::Plugin::Views

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


This package defines schemas and utils for storage plugin views.

A view in this context is what defines how data should be displayed to
users. Views are specified via a nested hash in Perl, which is then
serialized into JSON.

Right now, the only such view that can be defined is a "form" view.
A form view is simply the representation of a single record; in the
context of storage plugins here, this would be the form that opens
when you create or edit a single storage configuration entry in the
UI.

This commit adds a versioned JSON schema for storage plugin form
views. The first version of this form view supports customizing the
"General" tab in the following ways:

- Adding various types of columns:
  - Regular columns
  - A "bottom" column (the wide column below the regular ones)
  - Columns in the advanced subsection
  - A "bottom" column in the advanced subsection

- Adding fields to those columns
  - A field always corresponds to a SectionConfig property
  - Fields are typed and share common attributes
    (readonly, required, default)
  - Specific field types have specialized attributes unique to them,
    e.g. 'string' supports setting a 'display-mode', which can be
    'text', 'textarea' or 'password' ('text' by default)

Because each field corresponds to a SectionConfig property, the
existing API calls for creating & editing storage config entries can
simply be reused.

The ultimate goal here is to allow custom storage plugin authors to
allow integrating their plugin into our GUI with minimal effort and
without ever having to write JavaScript code. In fact, not being able
to write JS is a hard requirement for this feature.

The form view schema will be used in further commits after this one.

Some additional context:

Most of this approach is inspired by my past experience wrangling ERP
systems [0]. The ERP system I was developing modules for in particular
defined *a lot* of data models which could all be represented via
several generalized view types (such as form, list, gantt chart,
kanban, etc.). This was possible because all of those data models
shared a common base model and consequently a common database
representation as well. While all the applications within the system
were different, the way they were built was the same.

Furthermore, the idea expressed in this commit here is a
simplification of the somewhat commonly used MVVM architectural
pattern [1], in case that helps with understanding.

[0]: https://en.wikipedia.org/wiki/Enterprise_resource_planning
[1]: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel

Signed-off-by: Max R. Carrara <m.carrara at proxmox.com>
---
 src/PVE/Storage/Plugin/Makefile |   1 +
 src/PVE/Storage/Plugin/Views.pm | 242 ++++++++++++++++++++++++++++++++
 2 files changed, 243 insertions(+)
 create mode 100644 src/PVE/Storage/Plugin/Views.pm

diff --git a/src/PVE/Storage/Plugin/Makefile b/src/PVE/Storage/Plugin/Makefile
index ca82517..2e9b538 100644
--- a/src/PVE/Storage/Plugin/Makefile
+++ b/src/PVE/Storage/Plugin/Makefile
@@ -1,4 +1,5 @@
 SOURCES = Meta.pm		\
+	  Views.pm		\
 
 
 INSTALL_PATH = ${DESTDIR}${PERLDIR}/PVE/Storage/Plugin
diff --git a/src/PVE/Storage/Plugin/Views.pm b/src/PVE/Storage/Plugin/Views.pm
new file mode 100644
index 0000000..597c657
--- /dev/null
+++ b/src/PVE/Storage/Plugin/Views.pm
@@ -0,0 +1,242 @@
+package PVE::Storage::Plugin::Views;
+
+use v5.36;
+
+use Storable qw(dclone);
+
+use PVE::JSONSchema;
+
+use Exporter qw(import);
+
+our @EXPORT_OK = qw(
+    get_form_view_schema
+);
+
+=head1 NAME
+
+PVE::Storage::Plugin::Views - Schemas and Utils for Storage Plugin Views
+
+=head1 DESCRIPTION
+
+=for comment
+TODO
+
+=cut
+
+package PVE::Storage::Plugin::Views::v1 {
+    use v5.36;
+
+    use Storable qw(dclone);
+
+    use PVE::JSONSchema;
+
+    my $FIELD_TYPES = [
+        'boolean', 'integer', 'number', 'string', 'selection',
+    ];
+
+    my $ATTRIBUTES_COMMON = {
+        required => {
+            type => 'boolean',
+            optional => 1,
+        },
+        readonly => {
+            type => 'boolean',
+            optional => 1,
+        },
+        # NOTE: Overridden per field type; specified here to make clear that
+        # this is a common attribute
+        default => {
+            type => 'any',
+            optional => 1,
+        },
+    };
+
+    my $ATTRIBUTES_BOOLEAN = {
+        'instance-types' => ['boolean'],
+        $ATTRIBUTES_COMMON->%*,
+        default => {
+            type => 'boolean',
+            optional => 1,
+        },
+    };
+
+    my $ATTRIBUTES_INTEGER = {
+        'instance-types' => ['integer'],
+        $ATTRIBUTES_COMMON->%*,
+        default => {
+            type => 'integer',
+            optional => 1,
+        },
+    };
+
+    my $ATTRIBUTES_NUMBER = {
+        'instance-types' => ['number'],
+        $ATTRIBUTES_COMMON->%*,
+        default => {
+            type => 'number',
+            optional => 1,
+        },
+    };
+
+    my $ATTRIBUTES_STRING = {
+        'instance-types' => ['string'],
+        $ATTRIBUTES_COMMON->%*,
+        default => {
+            type => 'string',
+            optional => 1,
+        },
+        'display-mode' => {
+            type => 'string',
+            enum => ['text', 'textarea', 'password'],
+            optional => 1,
+            default => 'text',
+        },
+    };
+
+    my $ATTRIBUTES_SELECTION = {
+        'instance-types' => ['selection'],
+        $ATTRIBUTES_COMMON->%*,
+        'selection-mode' => {
+            type => 'string',
+            enum => ['single', 'multi'],
+            optional => 1,
+            default => 'single',
+        },
+        # List of "tuples" where the first element is the selection value,
+        # and the second element is how the selection value should be displayed to the user.
+        # For example:
+        # selection_values => [
+        #     ['gzip', "Compress using GZIP"],
+        #     ['zstd', "Compress using ZSTD"],
+        #     ['none', "No Compression"],
+        # ];
+        'selection-values' => {
+            type => 'array',
+            optional => 0,
+            items => {
+                type => 'array',
+                items => {
+                    type => 'string',
+                },
+            },
+        },
+        # The values selected by default on creation.
+        # Must exist in selection_values.
+        # If selection-mode is 'single', then only the first element is considered.
+        default => {
+            type => 'array',
+            items => {
+                type => 'string',
+            },
+            optional => 1,
+        },
+    };
+
+    my $FIELD_ATTRIBUTES_VARIANTS = [
+        $ATTRIBUTES_BOOLEAN,
+        $ATTRIBUTES_INTEGER,
+        $ATTRIBUTES_NUMBER,
+        $ATTRIBUTES_STRING,
+        $ATTRIBUTES_SELECTION,
+    ];
+
+    my $FIELD_SCHEMA = {
+        type => 'object',
+        properties => {
+            property => {
+                type => 'string',
+                optional => 0,
+            },
+            'field-type' => {
+                type => 'string',
+                enum => $FIELD_TYPES,
+                optional => 0,
+            },
+            label => {
+                type => 'string',
+                optional => 0,
+            },
+            attributes => {
+                type => 'object',
+                'type-property' => 'field-type',
+                oneOf => $FIELD_ATTRIBUTES_VARIANTS,
+                optional => 0,
+            },
+        },
+    };
+
+    my $COLUMN_SCHEMA = {
+        type => 'object',
+        properties => {
+            fields => {
+                type => 'array',
+                items => $FIELD_SCHEMA,
+                optional => 1,
+            },
+        },
+    };
+
+    my $FORM_VIEW_SCHEMA = {
+        type => 'object',
+        properties => {
+            general => {
+                type => 'object',
+                optional => 1,
+                properties => {
+                    columns => {
+                        type => 'array',
+                        items => $COLUMN_SCHEMA,
+                        optional => 1,
+                    },
+                    'column-bottom' => {
+                        $COLUMN_SCHEMA->%*, optional => 1,
+                    },
+                    'columns-advanced' => {
+                        type => 'array',
+                        items => $COLUMN_SCHEMA,
+                        optional => 1,
+                    },
+                    'column-advanced-bottom' => {
+                        $COLUMN_SCHEMA->%*, optional => 1,
+                    },
+                },
+            },
+        },
+    };
+
+    PVE::JSONSchema::validate_schema($FORM_VIEW_SCHEMA);
+
+    sub get_form_view_schema() {
+        return dclone($FORM_VIEW_SCHEMA);
+    }
+};
+
+my $API_FORM_VIEW_SCHEMA = {
+    type => 'object',
+    properties => {
+        version => {
+            type => 'integer',
+            enum => [1],
+            optional => 0,
+        },
+        definition => {
+            type => 'object',
+            'type-property' => 'version',
+            optional => 0,
+            oneOf => [
+                {
+                    'instance-types' => [1],
+                    PVE::Storage::Plugin::Views::v1::get_form_view_schema()->%*,
+                },
+            ],
+        },
+    },
+};
+
+PVE::JSONSchema::validate_schema($API_FORM_VIEW_SCHEMA);
+
+sub get_form_view_schema() {
+    return dclone($API_FORM_VIEW_SCHEMA);
+}
+
+1;
-- 
2.47.2





More information about the pve-devel mailing list