From: "Max R. Carrara" <m.carrara@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [RFC pve-storage master v1 04/12] plugin: views: add package PVE::Storage::Plugin::Views
Date: Mon, 8 Sep 2025 20:00:48 +0200 [thread overview]
Message-ID: <20250908180058.530119-5-m.carrara@proxmox.com> (raw)
In-Reply-To: <20250908180058.530119-1-m.carrara@proxmox.com>
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@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
_______________________________________________
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 ` Max R. Carrara [this message]
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 ` [pve-devel] [RFC pve-manager master v1 10/12] ui: storage: add CustomBase.js Max R. Carrara
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-5-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