* [pdm-devel] [PATCH proxmox v3 1/5] router/api-macro: add unstable flag for ApiMethod
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
@ 2025-11-19 14:05 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox v3 2/5] pve-api-types: generate array objects Shannon Sterz
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:05 UTC (permalink / raw)
To: pdm-devel
this allows marking certain api endpoints as unstable. an unstable api
endpoint gives no guarantees that it will stay compatible between
different versions. programmers that make use of an unstable endpoint,
have to expect that anything is subject to change.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
proxmox-api-macro/src/api/method.rs | 9 ++++++++-
proxmox-router/src/router.rs | 10 ++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/proxmox-api-macro/src/api/method.rs b/proxmox-api-macro/src/api/method.rs
index d3d17447..68969735 100644
--- a/proxmox-api-macro/src/api/method.rs
+++ b/proxmox-api-macro/src/api/method.rs
@@ -245,6 +245,12 @@ pub fn handle_method(mut attribs: JSONObject, func: syn::ItemFn) -> Result<Token
.transpose()?
.unwrap_or(false);
+ let unstable: bool = attribs
+ .remove("unstable")
+ .map(TryFrom::try_from)
+ .transpose()?
+ .unwrap_or(false);
+
if !attribs.is_empty() {
error!(
attribs.span(),
@@ -329,7 +335,8 @@ pub fn handle_method(mut attribs: JSONObject, func: syn::ItemFn) -> Result<Token
#returns_schema_setter
#access_setter
.reload_timezone(#reload_timezone)
- .protected(#protected);
+ .protected(#protected)
+ .unstable(#unstable);
#default_consts
diff --git a/proxmox-router/src/router.rs b/proxmox-router/src/router.rs
index 732ced8b..f4207d2a 100644
--- a/proxmox-router/src/router.rs
+++ b/proxmox-router/src/router.rs
@@ -789,6 +789,8 @@ pub struct ApiMethod {
/// The protected flag indicates that the provides function should be forwarded
/// to the daemon running in privileged mode.
pub protected: bool,
+ /// Whether this method is still experimental or already stable.
+ pub unstable: bool,
/// This flag indicates that the provided method may change the local timezone, so the server
/// should do a tzset afterwards
pub reload_timezone: bool,
@@ -820,6 +822,7 @@ impl ApiMethod {
handler,
returns: ReturnType::new(false, &NULL_SCHEMA),
protected: false,
+ unstable: false,
reload_timezone: false,
access: ApiAccess {
description: None,
@@ -838,6 +841,7 @@ impl ApiMethod {
handler: &DUMMY_HANDLER,
returns: ReturnType::new(false, &NULL_SCHEMA),
protected: false,
+ unstable: false,
reload_timezone: false,
access: ApiAccess {
description: None,
@@ -858,6 +862,12 @@ impl ApiMethod {
self
}
+ pub const fn unstable(mut self, unstable: bool) -> Self {
+ self.unstable = unstable;
+
+ self
+ }
+
pub const fn reload_timezone(mut self, reload_timezone: bool) -> Self {
self.reload_timezone = reload_timezone;
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH proxmox v3 2/5] pve-api-types: generate array objects
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
2025-11-19 14:05 ` [pdm-devel] [PATCH proxmox v3 1/5] router/api-macro: add unstable flag for ApiMethod Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox v3 3/5] pve-api-types: fix clippy lints Shannon Sterz
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
Previously the array objects had their *item* as schema, which is
wrong.
We now generate the object schema by pointing to the item schemas for
each field.
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
[SS: minor clean up in commit message]
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
pve-api-types/src/types/macros.rs | 148 +++++++++++++++++++++++++++++-
pve-api-types/src/types/mod.rs | 3 +
2 files changed, 149 insertions(+), 2 deletions(-)
diff --git a/pve-api-types/src/types/macros.rs b/pve-api-types/src/types/macros.rs
index 32a04b66..e6116782 100644
--- a/pve-api-types/src/types/macros.rs
+++ b/pve-api-types/src/types/macros.rs
@@ -1,3 +1,124 @@
+use proxmox_schema::SchemaPropertyEntry;
+
+pub(crate) const __DIGIT_SPACE: usize = 4;
+
+/// Since our object schemas need lexicographically sorted names, we generate *those* indices
+/// separately.
+///
+/// The idea is as follows:
+/// - If we can attach a zero without going out of bounds, that's the next number.
+/// (1 => 10 => 100 => 1000, and so on)
+/// - Otherwise, repeat until we end up at zero (which is the end):
+/// - If the number does not end in a `9`, we can just increment. If we don't exceed the limit,
+/// return the number.
+/// (3 => 4, 134 => 135, 3850 => 3851, and so on)
+///
+/// - If it does end with a `9`, cut it off:
+/// (1299 => 129 => 12, 14399 => 1439 => 143)
+const fn next_lexicographical_number(mut at: usize, count: usize) -> Option<usize> {
+ // special case since `0 * 10` is still 0 ;-)
+ if at == 0 {
+ return Some(1);
+ }
+
+ let longer = at * 10;
+ if longer < count {
+ return Some(longer);
+ }
+
+ while at != 0 {
+ if at % 10 != 9 {
+ at += 1;
+ if at < count {
+ return Some(at);
+ }
+ }
+ at /= 10;
+ }
+
+ None
+}
+
+/// Equivalent to `write!(to, "{name}{index}")`.
+pub(crate) const fn write_name_index(to: &mut [u8], name: &'static str, mut index: usize) {
+ let name = name.as_bytes();
+ let mut len = 0;
+ while len != name.len() {
+ to[len] = name[len];
+ len += 1;
+ }
+ if index == 0 {
+ to[len] = b'0';
+ len += 1;
+ } else {
+ let mut digits = 0;
+ let mut copy = index;
+ while copy != 0 {
+ digits += 1;
+ copy /= 10;
+ }
+ len += digits;
+
+ let mut at = len - 1;
+ while index != 0 {
+ to[at] = b'0' + (index % 10) as u8;
+ index /= 10;
+ at -= 1;
+ }
+ }
+}
+
+/// Fill the buffer in `data` with `prefix0`, `prefix1`, `prefix2`, ... - but sorted
+/// lexicographically!
+pub(crate) const fn __fill_names<const N: usize>(prefix: &'static str, data: &mut [u8]) {
+ let unit_size = __DIGIT_SPACE + prefix.len();
+
+ let mut item = 0;
+ let mut sorted_index = Some(0);
+ while item != N {
+ let at = item * unit_size;
+
+ let (_, slot) = data.split_at_mut(at);
+ match sorted_index {
+ None => panic!("ran out of indices"),
+ Some(index) => {
+ write_name_index(slot, prefix, index);
+ sorted_index = next_lexicographical_number(index, N);
+ }
+ }
+
+ item += 1;
+ }
+}
+
+/// Assuming `data` is now an array of field names, perform the equivalent of:
+///
+/// `properties[N].0 = fields[N] foreach N;`
+pub(crate) const fn __fill_properties<const N: usize>(
+ prefix: &'static str,
+ mut data: &'static [u8],
+ properties: &mut [SchemaPropertyEntry; N],
+) {
+ let unit_size = __DIGIT_SPACE + prefix.len();
+ let mut item = 0;
+ while item != N {
+ let slot;
+ (slot, data) = data.split_at(unit_size);
+ let mut len = 0;
+ while len != unit_size && slot[len] != 0 {
+ len += 1;
+ }
+ let slot = slot.split_at(len).0;
+
+ match std::str::from_utf8(slot) {
+ Ok(field_name) => properties[item].0 = field_name,
+ Err(_) => panic!("non utf-8 field"),
+ }
+
+ item += 1;
+ }
+}
+
macro_rules! generate_array_field {
($type_name:ident [ $array_len:expr ] :
$doc:expr,
@@ -12,11 +133,34 @@ macro_rules! generate_array_field {
impl $type_name {
pub const MAX: usize = $array_len;
+
+ const ITEM_SCHEMA: ::proxmox_schema::Schema =
+ ::proxmox_api_macro::json_schema! $api_def ;
+
+ const ARRAY_OBJECT_SCHEMA: Schema = const {
+ const BUFSIZE: usize = (stringify!($field_prefix).len() + $crate::types::__DIGIT_SPACE) * $array_len;
+
+ const NAMES: [u8; BUFSIZE] = const {
+ let mut buffer = [0u8; BUFSIZE];
+ $crate::types::__fill_names::<$array_len>(stringify!($field_prefix), &mut buffer);
+ buffer
+ };
+
+ const PROPERTIES: [::proxmox_schema::SchemaPropertyEntry; $array_len] = const {
+ let mut properties = [("", false, &$type_name::ITEM_SCHEMA); $array_len];
+ $crate::types::__fill_properties::<$array_len>(stringify!($field_prefix), &NAMES, &mut properties);
+ properties
+ };
+
+ ::proxmox_schema::ObjectSchema::new(
+ concat!("Container for the `", stringify!($field_prefix), "[N]` fields."),
+ &PROPERTIES,
+ ).schema()
+ };
}
impl ::proxmox_schema::ApiType for $type_name {
- const API_SCHEMA: ::proxmox_schema::Schema =
- ::proxmox_api_macro::json_schema! $api_def ;
+ const API_SCHEMA: ::proxmox_schema::Schema = Self::ARRAY_OBJECT_SCHEMA;
}
impl std::ops::Deref for $type_name {
diff --git a/pve-api-types/src/types/mod.rs b/pve-api-types/src/types/mod.rs
index 63209d84..37460cdf 100644
--- a/pve-api-types/src/types/mod.rs
+++ b/pve-api-types/src/types/mod.rs
@@ -11,7 +11,10 @@ use serde_json::Value;
use proxmox_schema::{api, const_regex, ApiStringFormat, ApiType, Schema, StringSchema};
mod macros;
+pub(crate) use macros::__fill_names;
+pub(crate) use macros::__fill_properties;
use macros::generate_array_field;
+pub(crate) use macros::__DIGIT_SPACE;
pub mod array;
pub mod stringlist;
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH proxmox v3 3/5] pve-api-types: fix clippy lints
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
2025-11-19 14:05 ` [pdm-devel] [PATCH proxmox v3 1/5] router/api-macro: add unstable flag for ApiMethod Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox v3 2/5] pve-api-types: generate array objects Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox v3 4/5] docgen: add docgen crate Shannon Sterz
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
by adding liftime parameters and removing an unnecessary assignment.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
pve-api-types/src/types/array.rs | 4 ++--
pve-api-types/src/types/macros.rs | 1 -
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/pve-api-types/src/types/array.rs b/pve-api-types/src/types/array.rs
index d17fa992..6f468fbf 100644
--- a/pve-api-types/src/types/array.rs
+++ b/pve-api-types/src/types/array.rs
@@ -89,12 +89,12 @@ impl<T, const MAX: usize> ArrayMap<T, { MAX }> {
}
/// Iterator through `(index, &value)` pairs.
- pub fn iter(&self) -> btree_map::Iter<usize, T> {
+ pub fn iter(&self) -> btree_map::Iter<'_, usize, T> {
self.inner.iter()
}
/// Iterator through `(index, &mut value)` pairs.
- pub fn iter_mut(&mut self) -> btree_map::IterMut<usize, T> {
+ pub fn iter_mut(&mut self) -> btree_map::IterMut<'_, usize, T> {
self.inner.iter_mut()
}
diff --git a/pve-api-types/src/types/macros.rs b/pve-api-types/src/types/macros.rs
index e6116782..8e5c3f02 100644
--- a/pve-api-types/src/types/macros.rs
+++ b/pve-api-types/src/types/macros.rs
@@ -49,7 +49,6 @@ pub(crate) const fn write_name_index(to: &mut [u8], name: &'static str, mut inde
}
if index == 0 {
to[len] = b'0';
- len += 1;
} else {
let mut digits = 0;
let mut copy = index;
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH proxmox v3 4/5] docgen: add docgen crate
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
` (2 preceding siblings ...)
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox v3 3/5] pve-api-types: fix clippy lints Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox v3 5/5] docgen: add support for the new stable flag Shannon Sterz
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
currently this is limited to dumping a `Router` into a `Value` which
can be serialized to JSON and interpreted by an api viewer.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
Cargo.toml | 1 +
proxmox-docgen/Cargo.toml | 20 ++
proxmox-docgen/debian/changelog | 5 +
proxmox-docgen/debian/control | 36 +++
proxmox-docgen/debian/copyright | 18 ++
proxmox-docgen/debian/debcargo.toml | 7 +
proxmox-docgen/src/lib.rs | 329 ++++++++++++++++++++++++++++
7 files changed, 416 insertions(+)
create mode 100644 proxmox-docgen/Cargo.toml
create mode 100644 proxmox-docgen/debian/changelog
create mode 100644 proxmox-docgen/debian/control
create mode 100644 proxmox-docgen/debian/copyright
create mode 100644 proxmox-docgen/debian/debcargo.toml
create mode 100644 proxmox-docgen/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index 42994992..bc4a3b09 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,7 @@ members = [
"proxmox-config-digest",
"proxmox-daemon",
"proxmox-dns-api",
+ "proxmox-docgen",
"proxmox-http",
"proxmox-http-error",
"proxmox-human-byte",
diff --git a/proxmox-docgen/Cargo.toml b/proxmox-docgen/Cargo.toml
new file mode 100644
index 00000000..84a515de
--- /dev/null
+++ b/proxmox-docgen/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "proxmox-docgen"
+description = "Library to easily create documentation from Rust sources for Proxmox projects."
+version = "1.0.0"
+
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+serde_json.workspace = true
+
+proxmox-router.workspace = true
+proxmox-schema = { workspace = true }
+
+
diff --git a/proxmox-docgen/debian/changelog b/proxmox-docgen/debian/changelog
new file mode 100644
index 00000000..79b7e602
--- /dev/null
+++ b/proxmox-docgen/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-docgen (1.0.0-1) trixie; urgency=medium
+
+ * Initial packaging
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 10 Nov 2025 10:44:26 +0200
diff --git a/proxmox-docgen/debian/control b/proxmox-docgen/debian/control
new file mode 100644
index 00000000..4110e9e0
--- /dev/null
+++ b/proxmox-docgen/debian/control
@@ -0,0 +1,36 @@
+Source: rust-proxmox-docgen
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-proxmox-router-3+default-dev (>= 3.2.2-~~) <!nocheck>,
+ librust-proxmox-schema-5+default-dev (>= 5.0.1-~~) <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-docgen
+
+Package: librust-proxmox-docgen-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-router-3+default-dev (>= 3.2.2-~~),
+ librust-proxmox-schema-5+default-dev (>= 5.0.1-~~),
+ librust-serde-json-1+default-dev
+Provides:
+ librust-proxmox-docgen+default-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1+default-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1.0-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1.0+default-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1.0.0-dev (= ${binary:Version}),
+ librust-proxmox-docgen-1.0.0+default-dev (= ${binary:Version})
+Description: Easily create documentation from Rust sources for Proxmox projects - Rust source code
+ Source code for Debianized Rust crate "proxmox-docgen"
diff --git a/proxmox-docgen/debian/copyright b/proxmox-docgen/debian/copyright
new file mode 100644
index 00000000..d6e3c304
--- /dev/null
+++ b/proxmox-docgen/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2025 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-docgen/debian/debcargo.toml b/proxmox-docgen/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-docgen/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-docgen/src/lib.rs b/proxmox-docgen/src/lib.rs
new file mode 100644
index 00000000..df8077f8
--- /dev/null
+++ b/proxmox-docgen/src/lib.rs
@@ -0,0 +1,329 @@
+use serde_json::{json, Value};
+
+use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute};
+use proxmox_schema::format::get_property_string_type_text;
+use proxmox_schema::{ApiStringFormat, ObjectSchemaType, Schema};
+
+/// Generate a `sede_json::Value` that represents an API in a tree-like structure.
+///
+/// - `router`: Specifies to root `Router` of the API that should be dumped.
+/// - `path`: Defines the base path that will considered as the root of this API (sub-)tree. If
+/// `"."` is used, the tree will be considered as starting from the main root (`"/"`).
+/// - `privileges`: A slice of tuples that will be used to translate the internal representation of
+/// a privilege as a `u64` to it's human readable name.
+pub fn generate_api_tree(router: &Router, path: &str, privileges: &[(&str, u64)]) -> Value {
+ let mut data = dump_api_schema(router, path, privileges);
+ data["expanded"] = true.into();
+ data
+}
+
+fn dump_schema(schema: &Schema) -> Value {
+ let mut data;
+
+ match schema {
+ Schema::Null => {
+ data = json!({
+ "type": "null",
+ });
+ }
+ Schema::Boolean(boolean_schema) => {
+ data = json!({
+ "type": "boolean",
+ "description": boolean_schema.description,
+ });
+ if let Some(default) = boolean_schema.default {
+ data["default"] = default.into();
+ }
+ }
+ Schema::String(string_schema) => {
+ data = json!({
+ "type": "string",
+ "description": string_schema.description,
+ });
+ if let Some(default) = string_schema.default {
+ data["default"] = default.into();
+ }
+ if let Some(min_length) = string_schema.min_length {
+ data["minLength"] = min_length.into();
+ }
+ if let Some(max_length) = string_schema.max_length {
+ data["maxLength"] = max_length.into();
+ }
+ if let Some(type_text) = string_schema.type_text {
+ data["typetext"] = type_text.into();
+ }
+ match string_schema.format {
+ None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
+ Some(ApiStringFormat::Pattern(const_regex)) => {
+ data["pattern"] = format!("/{}/", const_regex.regex_string).into();
+ }
+ Some(ApiStringFormat::Enum(variants)) => {
+ let variants: Vec<String> =
+ variants.iter().map(|e| e.value.to_string()).collect();
+ data["enum"] = serde_json::to_value(variants).unwrap();
+ }
+ Some(ApiStringFormat::PropertyString(subschema)) => {
+ match subschema {
+ Schema::Object(_) | Schema::Array(_) => {
+ data["format"] = dump_schema(subschema);
+ data["typetext"] = get_property_string_type_text(subschema).into();
+ }
+ _ => { /* do nothing - should not happen */ }
+ };
+ }
+ }
+ }
+ Schema::Integer(integer_schema) => {
+ data = json!({
+ "type": "integer",
+ "description": integer_schema.description,
+ });
+ if let Some(default) = integer_schema.default {
+ data["default"] = default.into();
+ }
+ if let Some(minimum) = integer_schema.minimum {
+ data["minimum"] = minimum.into();
+ }
+ if let Some(maximum) = integer_schema.maximum {
+ data["maximum"] = maximum.into();
+ }
+ }
+ Schema::Number(number_schema) => {
+ data = json!({
+ "type": "number",
+ "description": number_schema.description,
+ });
+ if let Some(default) = number_schema.default {
+ data["default"] = default.into();
+ }
+ if let Some(minimum) = number_schema.minimum {
+ data["minimum"] = minimum.into();
+ }
+ if let Some(maximum) = number_schema.maximum {
+ data["maximum"] = maximum.into();
+ }
+ }
+ Schema::Object(object_schema) => {
+ data = dump_property_schema(object_schema);
+ data["type"] = "object".into();
+ if let Some(default_key) = object_schema.default_key {
+ data["default_key"] = default_key.into();
+ }
+ }
+ Schema::Array(array_schema) => {
+ data = json!({
+ "type": "array",
+ "description": array_schema.description,
+ "items": dump_schema(array_schema.items),
+ });
+ if let Some(min_length) = array_schema.min_length {
+ data["minLength"] = min_length.into();
+ }
+ if let Some(max_length) = array_schema.min_length {
+ data["maxLength"] = max_length.into();
+ }
+ }
+ Schema::AllOf(alloff_schema) => {
+ data = dump_property_schema(alloff_schema);
+ data["type"] = "object".into();
+ }
+ Schema::OneOf(schema) => {
+ let mut type_schema = dump_schema(schema.type_schema());
+ if schema.type_property_entry.1 {
+ type_schema["optional"] = true.into();
+ }
+ data = json!({
+ "type": "object",
+ "description": schema.description,
+ "typeProperty": schema.type_property(),
+ "typeSchema": type_schema,
+ });
+ let mut variants = Vec::with_capacity(schema.list.len());
+ for (title, variant) in schema.list {
+ let mut entry = dump_schema(variant);
+ entry["title"] = (*title).into();
+ variants.push(entry);
+ }
+ data["oneOf"] = variants.into();
+ }
+ };
+
+ data
+}
+
+fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
+ let mut properties = json!({});
+
+ for (prop, optional, schema) in param.properties() {
+ let mut property = dump_schema(schema);
+ if *optional {
+ property["optional"] = 1.into();
+ }
+ properties[prop] = property;
+ }
+
+ let data = json!({
+ "description": param.description(),
+ "additionalProperties": param.additional_properties(),
+ "properties": properties,
+ });
+
+ data
+}
+
+fn dump_api_permission(permission: &Permission, privileges: &[(&str, u64)]) -> Value {
+ match permission {
+ Permission::Superuser => json!({ "user": "root@pam" }),
+ Permission::User(user) => json!({ "user": user }),
+ Permission::Anybody => json!({ "user": "all" }),
+ Permission::World => json!({ "user": "world" }),
+ Permission::UserParam(param) => json!({ "userParam": param }),
+ Permission::Group(group) => json!({ "group": group }),
+ Permission::WithParam(param, sub_permission) => {
+ json!({
+ "withParam": {
+ "name": param,
+ "permissions": dump_api_permission(sub_permission, privileges),
+ },
+ })
+ }
+ Permission::Privilege(name, value, partial) => {
+ let mut privs = Vec::new();
+ for (name, v) in privileges {
+ if (value & v) != 0 {
+ privs.push(name.to_string());
+ }
+ }
+
+ json!({
+ "check": {
+ "path": name,
+ "privs": privs,
+ "partial": partial,
+ }
+ })
+ }
+ Permission::And(list) => {
+ let list: Vec<Value> = list
+ .iter()
+ .map(|p| dump_api_permission(p, privileges))
+ .collect();
+ json!({ "and": list })
+ }
+ Permission::Or(list) => {
+ let list: Vec<Value> = list
+ .iter()
+ .map(|p| dump_api_permission(p, privileges))
+ .collect();
+ json!({ "or": list })
+ }
+ }
+}
+
+fn dump_api_method_schema(
+ method: &str,
+ api_method: &ApiMethod,
+ privileges: &[(&str, u64)],
+) -> Value {
+ let mut data = json!({
+ "description": api_method.parameters.description(),
+ });
+
+ data["parameters"] = dump_property_schema(&api_method.parameters);
+
+ let mut returns = dump_schema(api_method.returns.schema);
+ if api_method.returns.optional {
+ returns["optional"] = 1.into();
+ }
+ data["returns"] = returns;
+
+ match api_method.access {
+ ApiAccess {
+ description: None,
+ permission: Permission::Superuser,
+ } => {
+ // no need to output default
+ }
+ ApiAccess {
+ description,
+ permission,
+ } => {
+ let mut permissions = dump_api_permission(permission, privileges);
+ if let Some(description) = description {
+ permissions["description"] = description.into();
+ }
+ data["permissions"] = permissions;
+ }
+ }
+
+ let mut method = method;
+
+ if let ApiHandler::AsyncHttp(_) = api_method.handler {
+ method = if method == "POST" { "UPLOAD" } else { method };
+ method = if method == "GET" { "DOWNLOAD" } else { method };
+ }
+
+ data["method"] = method.into();
+
+ data
+}
+
+fn dump_api_schema(router: &Router, path: &str, privileges: &[(&str, u64)]) -> Value {
+ let mut data = json!({});
+
+ let mut info = json!({});
+ if let Some(api_method) = router.get {
+ info["GET"] = dump_api_method_schema("GET", api_method, privileges);
+ }
+ if let Some(api_method) = router.post {
+ info["POST"] = dump_api_method_schema("POST", api_method, privileges);
+ }
+ if let Some(api_method) = router.put {
+ info["PUT"] = dump_api_method_schema("PUT", api_method, privileges);
+ }
+ if let Some(api_method) = router.delete {
+ info["DELETE"] = dump_api_method_schema("DELETE", api_method, privileges);
+ }
+
+ data["info"] = info;
+
+ match &router.subroute {
+ None => {
+ data["leaf"] = 1.into();
+ }
+ Some(SubRoute::MatchAll { router, param_name }) => {
+ let sub_path = if path == "." {
+ format!("/{{{param_name}}}")
+ } else {
+ format!("{path}/{{{param_name}}}")
+ };
+ let mut child = dump_api_schema(router, &sub_path, privileges);
+ child["path"] = sub_path.into();
+ child["text"] = format!("{{{param_name}}}").into();
+
+ let children = vec![child];
+ data["children"] = children.into();
+ data["leaf"] = 0.into();
+ }
+ Some(SubRoute::Map(dirmap)) => {
+ let mut children = Vec::new();
+
+ for (key, sub_router) in dirmap.iter() {
+ let sub_path = if path == "." {
+ format!("/{key}")
+ } else {
+ format!("{path}/{key}")
+ };
+ let mut child = dump_api_schema(sub_router, &sub_path, privileges);
+ child["path"] = sub_path.into();
+ child["text"] = key.to_string().into();
+ children.push(child);
+ }
+
+ data["children"] = children.into();
+ data["leaf"] = 0.into();
+ }
+ }
+
+ data
+}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH proxmox v3 5/5] docgen: add support for the new stable flag
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
` (3 preceding siblings ...)
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox v3 4/5] docgen: add docgen crate Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH widget-toolkit v3 1/1] api viewer: add support for endpoints that are marked as unstable Shannon Sterz
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
proxmox-docgen/src/lib.rs | 1 +
1 file changed, 1 insertion(+)
diff --git a/proxmox-docgen/src/lib.rs b/proxmox-docgen/src/lib.rs
index df8077f8..f4e7bf1e 100644
--- a/proxmox-docgen/src/lib.rs
+++ b/proxmox-docgen/src/lib.rs
@@ -236,6 +236,7 @@ fn dump_api_method_schema(
returns["optional"] = 1.into();
}
data["returns"] = returns;
+ data["unstable"] = api_method.unstable.into();
match api_method.access {
ApiAccess {
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH widget-toolkit v3 1/1] api viewer: add support for endpoints that are marked as unstable
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
` (4 preceding siblings ...)
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox v3 5/5] docgen: add support for the new stable flag Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox-backup v3 1/2] docgen: use proxmox-rs docgen crate Shannon Sterz
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
display a warning when an endpoint is marked as unstable.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
src/api-viewer/APIViewer.js | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/src/api-viewer/APIViewer.js b/src/api-viewer/APIViewer.js
index 7f27e0d..1357741 100644
--- a/src/api-viewer/APIViewer.js
+++ b/src/api-viewer/APIViewer.js
@@ -210,7 +210,21 @@ Ext.onReady(function () {
usage += cliUsageRenderer(method, endpoint);
}
- let sections = [
+ let sections = [];
+
+ if (info.unstable) {
+ sections.push({
+ title: 'Unstable',
+ html: `<div class="proxmox-warning-row" style="padding: 10px;">
+ <i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
+ This API endpoint is marked as unstable. All information on this
+ page is subject to change, including input parameters, return values
+ and permissions.
+ </div>`,
+ });
+ }
+
+ sections.concat([
{
title: 'Description',
html: Ext.htmlEncode(info.description),
@@ -221,7 +235,7 @@ Ext.onReady(function () {
html: usage,
bodyPadding: 10,
},
- ];
+ ]);
if (info.parameters && info.parameters.properties) {
let pstore = Ext.create('Ext.data.Store', {
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH proxmox-backup v3 1/2] docgen: use proxmox-rs docgen crate
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
` (5 preceding siblings ...)
2025-11-19 14:06 ` [pdm-devel] [PATCH widget-toolkit v3 1/1] api viewer: add support for endpoints that are marked as unstable Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox-backup v3 2/2] notifications/pbsXtoX: adapt to proxmox-apt making old_version optional Shannon Sterz
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
this has the benefit of also handling the unstable flag correctly.
hence, the api viewer needs to add the css definitions to properly
render unstable endpoints.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
Cargo.toml | 3 +
docs/api-viewer/index.html | 2 +
src/bin/docgen.rs | 326 ++-----------------------------------
3 files changed, 20 insertions(+), 311 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 28fdbf54..fbd16ccb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -62,6 +62,7 @@ proxmox-borrow = "1"
proxmox-compression = "1.0.1"
proxmox-config-digest = "1"
proxmox-daemon = "1"
+proxmox-docgen = "1"
proxmox-fuse = "1"
proxmox-http = { version = "1.0.2", features = [ "client", "http-helpers", "websocket" ] } # see below
proxmox-human-byte = "1"
@@ -216,6 +217,7 @@ proxmox-base64.workspace = true
proxmox-compression.workspace = true
proxmox-config-digest.workspace = true
proxmox-daemon.workspace = true
+proxmox-docgen.workspace = true
proxmox-http = { workspace = true, features = [ "body", "client-trait", "proxmox-async", "rate-limited-stream" ] } # pbs-client doesn't use these
proxmox-human-byte.workspace = true
proxmox-io.workspace = true
@@ -275,6 +277,7 @@ proxmox-rrd-api-types.workspace = true
#proxmox-compression = { path = "../proxmox/proxmox-compression" }
#proxmox-config-digest = { path = "../proxmox/proxmox-config-digest" }
#proxmox-daemon = { path = "../proxmox/proxmox-daemon" }
+#proxmox-docgen = { path = "../proxmox/proxmox-docgen" }
#proxmox-http = { path = "../proxmox/proxmox-http" }
#proxmox-http-error = { path = "../proxmox/proxmox-http-error" }
#proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" }
diff --git a/docs/api-viewer/index.html b/docs/api-viewer/index.html
index 72dae96a..28f5b3b7 100644
--- a/docs/api-viewer/index.html
+++ b/docs/api-viewer/index.html
@@ -6,6 +6,8 @@
<title>Proxmox Backup Server API Documentation</title>
<link rel="stylesheet" type="text/css" href="extjs/theme-crisp/resources/theme-crisp-all.css">
+ <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
+ <link rel="stylesheet" type="text/css" href="/widgettoolkit/css/ext6-pmx.css" />
<link rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" href="/widgettoolkit/themes/theme-proxmox-dark.css" />
<script type="text/javascript" src="extjs/ext-all.js"></script>
<script type="text/javascript" src="apidoc.js"></script>
diff --git a/src/bin/docgen.rs b/src/bin/docgen.rs
index 674be945..04d2de29 100644
--- a/src/bin/docgen.rs
+++ b/src/bin/docgen.rs
@@ -1,9 +1,7 @@
use anyhow::{bail, Error};
-use serde_json::{json, Value};
-use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute};
-use proxmox_schema::format::{dump_enum_properties, get_property_string_type_text};
-use proxmox_schema::{ApiStringFormat, ApiType, ObjectSchemaType, Schema};
+use proxmox_schema::format::dump_enum_properties;
+use proxmox_schema::ApiType;
use proxmox_section_config::dump_section_config;
use pbs_api_types::PRIVILEGES;
@@ -55,22 +53,30 @@ fn main() -> Result<(), Error> {
fn generate_api_tree() -> String {
let mut tree = Vec::new();
- let mut data = dump_api_schema(&api2::ROUTER, ".");
+ let mut data = proxmox_docgen::generate_api_tree(&api2::ROUTER, ".", PRIVILEGES);
data["path"] = "/".into();
// hack: add invisible space to sort as first entry
data["text"] = "​Management API (HTTP)".into();
- data["expanded"] = true.into();
-
tree.push(data);
- let mut data = dump_api_schema(&api2::backup::BACKUP_API_ROUTER, "/backup/_upgrade_");
+ let mut data = proxmox_docgen::generate_api_tree(
+ &api2::backup::BACKUP_API_ROUTER,
+ "/backup/_upgrade_",
+ PRIVILEGES,
+ );
data["path"] = "/backup/_upgrade_".into();
data["text"] = "Backup API (HTTP/2)".into();
+ data["expanded"] = false.into();
tree.push(data);
- let mut data = dump_api_schema(&api2::reader::READER_API_ROUTER, "/reader/_upgrade_");
+ let mut data = proxmox_docgen::generate_api_tree(
+ &api2::reader::READER_API_ROUTER,
+ "/reader/_upgrade_",
+ PRIVILEGES,
+ );
data["path"] = "/reader/_upgrade_".into();
data["text"] = "Restore API (HTTP/2)".into();
+ data["expanded"] = false.into();
tree.push(data);
format!(
@@ -78,305 +84,3 @@ fn generate_api_tree() -> String {
serde_json::to_string_pretty(&tree).unwrap()
)
}
-
-pub fn dump_schema(schema: &Schema) -> Value {
- let mut data;
-
- match schema {
- Schema::Null => {
- data = json!({
- "type": "null",
- });
- }
- Schema::Boolean(boolean_schema) => {
- data = json!({
- "type": "boolean",
- "description": boolean_schema.description,
- });
- if let Some(default) = boolean_schema.default {
- data["default"] = default.into();
- }
- }
- Schema::String(string_schema) => {
- data = json!({
- "type": "string",
- "description": string_schema.description,
- });
- if let Some(default) = string_schema.default {
- data["default"] = default.into();
- }
- if let Some(min_length) = string_schema.min_length {
- data["minLength"] = min_length.into();
- }
- if let Some(max_length) = string_schema.max_length {
- data["maxLength"] = max_length.into();
- }
- if let Some(type_text) = string_schema.type_text {
- data["typetext"] = type_text.into();
- }
- match string_schema.format {
- None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
- Some(ApiStringFormat::Pattern(const_regex)) => {
- data["pattern"] = format!("/{}/", const_regex.regex_string).into();
- }
- Some(ApiStringFormat::Enum(variants)) => {
- let variants: Vec<String> =
- variants.iter().map(|e| e.value.to_string()).collect();
- data["enum"] = serde_json::to_value(variants).unwrap();
- }
- Some(ApiStringFormat::PropertyString(subschema)) => {
- match subschema {
- Schema::Object(_) | Schema::Array(_) => {
- data["format"] = dump_schema(subschema);
- data["typetext"] = get_property_string_type_text(subschema).into();
- }
- _ => { /* do nothing - should not happen */ }
- };
- }
- }
- // fixme: dump format
- }
- Schema::Integer(integer_schema) => {
- data = json!({
- "type": "integer",
- "description": integer_schema.description,
- });
- if let Some(default) = integer_schema.default {
- data["default"] = default.into();
- }
- if let Some(minimum) = integer_schema.minimum {
- data["minimum"] = minimum.into();
- }
- if let Some(maximum) = integer_schema.maximum {
- data["maximum"] = maximum.into();
- }
- }
- Schema::Number(number_schema) => {
- data = json!({
- "type": "number",
- "description": number_schema.description,
- });
- if let Some(default) = number_schema.default {
- data["default"] = default.into();
- }
- if let Some(minimum) = number_schema.minimum {
- data["minimum"] = minimum.into();
- }
- if let Some(maximum) = number_schema.maximum {
- data["maximum"] = maximum.into();
- }
- }
- Schema::Object(object_schema) => {
- data = dump_property_schema(object_schema);
- data["type"] = "object".into();
- if let Some(default_key) = object_schema.default_key {
- data["default_key"] = default_key.into();
- }
- }
- Schema::Array(array_schema) => {
- data = json!({
- "type": "array",
- "description": array_schema.description,
- "items": dump_schema(array_schema.items),
- });
- if let Some(min_length) = array_schema.min_length {
- data["minLength"] = min_length.into();
- }
- if let Some(max_length) = array_schema.min_length {
- data["maxLength"] = max_length.into();
- }
- }
- Schema::AllOf(alloff_schema) => {
- data = dump_property_schema(alloff_schema);
- data["type"] = "object".into();
- }
- Schema::OneOf(schema) => {
- let mut type_schema = dump_schema(schema.type_schema());
- if schema.type_property_entry.1 {
- type_schema["optional"] = true.into();
- }
- data = json!({
- "type": "object",
- "description": schema.description,
- "typeProperty": schema.type_property(),
- "typeSchema": type_schema,
- });
- let mut variants = Vec::with_capacity(schema.list.len());
- for (title, variant) in schema.list {
- let mut entry = dump_schema(variant);
- entry["title"] = (*title).into();
- variants.push(entry);
- }
- data["oneOf"] = variants.into();
- }
- };
-
- data
-}
-
-pub fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
- let mut properties = json!({});
-
- for (prop, optional, schema) in param.properties() {
- let mut property = dump_schema(schema);
- if *optional {
- property["optional"] = 1.into();
- }
- properties[prop] = property;
- }
-
- let data = json!({
- "description": param.description(),
- "additionalProperties": param.additional_properties(),
- "properties": properties,
- });
-
- data
-}
-
-fn dump_api_permission(permission: &Permission) -> Value {
- match permission {
- Permission::Superuser => json!({ "user": "root@pam" }),
- Permission::User(user) => json!({ "user": user }),
- Permission::Anybody => json!({ "user": "all" }),
- Permission::World => json!({ "user": "world" }),
- Permission::UserParam(param) => json!({ "userParam": param }),
- Permission::Group(group) => json!({ "group": group }),
- Permission::WithParam(param, sub_permission) => {
- json!({
- "withParam": {
- "name": param,
- "permissions": dump_api_permission(sub_permission),
- },
- })
- }
- Permission::Privilege(name, value, partial) => {
- let mut privs = Vec::new();
- for (name, v) in PRIVILEGES {
- if (value & v) != 0 {
- privs.push(name.to_string());
- }
- }
-
- json!({
- "check": {
- "path": name,
- "privs": privs,
- "partial": partial,
- }
- })
- }
- Permission::And(list) => {
- let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
- json!({ "and": list })
- }
- Permission::Or(list) => {
- let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
- json!({ "or": list })
- }
- }
-}
-
-fn dump_api_method_schema(method: &str, api_method: &ApiMethod) -> Value {
- let mut data = json!({
- "description": api_method.parameters.description(),
- });
-
- data["parameters"] = dump_property_schema(&api_method.parameters);
-
- let mut returns = dump_schema(api_method.returns.schema);
- if api_method.returns.optional {
- returns["optional"] = 1.into();
- }
- data["returns"] = returns;
-
- match api_method.access {
- ApiAccess {
- description: None,
- permission: Permission::Superuser,
- } => {
- // no need to output default
- }
- ApiAccess {
- description,
- permission,
- } => {
- let mut permissions = dump_api_permission(permission);
- if let Some(description) = description {
- permissions["description"] = description.into();
- }
- data["permissions"] = permissions;
- }
- }
-
- let mut method = method;
-
- if let ApiHandler::AsyncHttp(_) = api_method.handler {
- method = if method == "POST" { "UPLOAD" } else { method };
- method = if method == "GET" { "DOWNLOAD" } else { method };
- }
-
- data["method"] = method.into();
-
- data
-}
-
-pub fn dump_api_schema(router: &Router, path: &str) -> Value {
- let mut data = json!({});
-
- let mut info = json!({});
- if let Some(api_method) = router.get {
- info["GET"] = dump_api_method_schema("GET", api_method);
- }
- if let Some(api_method) = router.post {
- info["POST"] = dump_api_method_schema("POST", api_method);
- }
- if let Some(api_method) = router.put {
- info["PUT"] = dump_api_method_schema("PUT", api_method);
- }
- if let Some(api_method) = router.delete {
- info["DELETE"] = dump_api_method_schema("DELETE", api_method);
- }
-
- data["info"] = info;
-
- match &router.subroute {
- None => {
- data["leaf"] = 1.into();
- }
- Some(SubRoute::MatchAll { router, param_name }) => {
- let sub_path = if path == "." {
- format!("/{{{param_name}}}")
- } else {
- format!("{path}/{{{param_name}}}")
- };
- let mut child = dump_api_schema(router, &sub_path);
- child["path"] = sub_path.into();
- child["text"] = format!("{{{param_name}}}").into();
-
- let children = vec![child];
- data["children"] = children.into();
- data["leaf"] = 0.into();
- }
- Some(SubRoute::Map(dirmap)) => {
- let mut children = Vec::new();
-
- for (key, sub_router) in dirmap.iter() {
- let sub_path = if path == "." {
- format!("/{key}")
- } else {
- format!("{path}/{key}")
- };
- let mut child = dump_api_schema(sub_router, &sub_path);
- child["path"] = sub_path.into();
- child["text"] = key.to_string().into();
- children.push(child);
- }
-
- data["children"] = children.into();
- data["leaf"] = 0.into();
- }
- }
-
- data
-}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH proxmox-backup v3 2/2] notifications/pbsXtoX: adapt to proxmox-apt making old_version optional
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
` (6 preceding siblings ...)
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox-backup v3 1/2] docgen: use proxmox-rs docgen crate Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH datacenter-manager v3 1/2] docgen: switch to proxmox-rs docgen crate Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH datacenter-manager v3 2/2] api-viewer: add an api-viewer package Shannon Sterz
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
src/bin/pbs2to3.rs | 4 ++--
src/bin/pbs3to4.rs | 4 ++--
src/server/notifications/template_data.rs | 2 +-
templates/default/package-updates-body.txt.hbs | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/bin/pbs2to3.rs b/src/bin/pbs2to3.rs
index f5a3cce6..6f9ad9d1 100644
--- a/src/bin/pbs2to3.rs
+++ b/src/bin/pbs2to3.rs
@@ -88,9 +88,9 @@ impl Checker {
.iter()
.find(|pkg| pkg.package.as_str() == PROXMOX_BACKUP_META);
- if let Some(pbs_meta_pkg) = pbs_meta_pkg {
+ if let Some(old_version) = pbs_meta_pkg.and_then(|m| m.old_version.as_ref()) {
let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?;
- let captures = pkg_version.captures(&pbs_meta_pkg.old_version);
+ let captures = pkg_version.captures(old_version);
if let Some(captures) = captures {
let maj = Self::extract_version_from_captures(1, &captures)?;
let min = Self::extract_version_from_captures(2, &captures)?;
diff --git a/src/bin/pbs3to4.rs b/src/bin/pbs3to4.rs
index 1d99a592..8f0bfc0d 100644
--- a/src/bin/pbs3to4.rs
+++ b/src/bin/pbs3to4.rs
@@ -89,9 +89,9 @@ impl Checker {
.iter()
.find(|pkg| pkg.package.as_str() == PROXMOX_BACKUP_META);
- if let Some(pbs_meta_pkg) = pbs_meta_pkg {
+ if let Some(old_version) = pbs_meta_pkg.and_then(|m| m.old_version.as_ref()) {
let pkg_version = Regex::new(r"^(\d+)\.(\d+)[.-](\d+)")?;
- let captures = pkg_version.captures(&pbs_meta_pkg.old_version);
+ let captures = pkg_version.captures(old_version);
if let Some(captures) = captures {
let maj = Self::extract_version_from_captures(1, &captures)?;
let min = Self::extract_version_from_captures(2, &captures)?;
diff --git a/src/server/notifications/template_data.rs b/src/server/notifications/template_data.rs
index 0dcc5ed3..88e49a03 100644
--- a/src/server/notifications/template_data.rs
+++ b/src/server/notifications/template_data.rs
@@ -154,7 +154,7 @@ pub struct UpgradablePackage {
/// The new version which can be installed.
available_version: String,
/// The currently installed version.
- installed_version: String,
+ installed_version: Option<String>,
}
/// Template data for the package-updates template.
diff --git a/templates/default/package-updates-body.txt.hbs b/templates/default/package-updates-body.txt.hbs
index 9800ab5c..61f5eec3 100644
--- a/templates/default/package-updates-body.txt.hbs
+++ b/templates/default/package-updates-body.txt.hbs
@@ -1,6 +1,6 @@
Proxmox Backup Server has the following updates available:
{{#each available-updates}}
- {{this.package-name}}: {{this.installed-version}} -> {{this.available-version}}
+ {{this.package-name}}: {{#if this.installed-version}}{{this.installed-version}} ->{{else}}NEW,{{/if}} {{this.available-version}}
{{/each}}
To upgrade visit the web interface:
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH datacenter-manager v3 1/2] docgen: switch to proxmox-rs docgen crate
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
` (7 preceding siblings ...)
2025-11-19 14:06 ` [pdm-devel] [PATCH proxmox-backup v3 2/2] notifications/pbsXtoX: adapt to proxmox-apt making old_version optional Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
2025-11-19 14:06 ` [pdm-devel] [PATCH datacenter-manager v3 2/2] api-viewer: add an api-viewer package Shannon Sterz
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
and use pdm permissions not pbs ones.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
Cargo.toml | 2 +
server/Cargo.toml | 1 +
server/src/bin/docgen.rs | 314 +--------------------------------------
3 files changed, 7 insertions(+), 310 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 3922041..6fc5f77 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-auth-api = "1.0.5"
proxmox-base64 = "1"
proxmox-client = "1"
proxmox-daemon = "1"
+proxmox-docgen = "1"
proxmox-http = { version = "1.0.4", features = [ "client", "http-helpers", "websocket" ] } # see below
proxmox-human-byte = "1"
proxmox-io = "1.0.1" # tools and client use "tokio" feature
@@ -154,6 +155,7 @@ zstd = { version = "0.13" }
# proxmox-config-digest = { path = "../proxmox/proxmox-config-digest" }
# proxmox-daemon = { path = "../proxmox/proxmox-daemon" }
# proxmox-dns-api = { path = "../proxmox/proxmox-dns-api" }
+# proxmox-docgen = { path = "../proxmox/proxmox-docgen" }
# proxmox-http-error = { path = "../proxmox/proxmox-http-error" }
# proxmox-http = { path = "../proxmox/proxmox-http" }
# proxmox-human-byte = { path = "../proxmox/proxmox-human-byte" }
diff --git a/server/Cargo.toml b/server/Cargo.toml
index b336f1a..6969549 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-async.workspace = true
proxmox-auth-api = { workspace = true, features = [ "api", "ticket", "pam-authenticator", "password-authenticator" ] }
proxmox-base64.workspace = true
proxmox-daemon.workspace = true
+proxmox-docgen.workspace = true
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
proxmox-lang.workspace = true
proxmox-ldap.workspace = true
diff --git a/server/src/bin/docgen.rs b/server/src/bin/docgen.rs
index cbf6fc0..85e05ec 100644
--- a/server/src/bin/docgen.rs
+++ b/server/src/bin/docgen.rs
@@ -1,12 +1,10 @@
use anyhow::{bail, Error};
-use serde_json::{json, Value};
-use proxmox_router::{ApiAccess, ApiHandler, ApiMethod, Permission, Router, SubRoute};
-use proxmox_schema::format::{dump_enum_properties, get_property_string_type_text};
-use proxmox_schema::{ApiStringFormat, ApiType, ObjectSchemaType, Schema};
+use proxmox_schema::format::dump_enum_properties;
+use proxmox_schema::ApiType;
use proxmox_section_config::{dump_section_config, typed::ApiSectionDataEntry};
-use pbs_api_types::PRIVILEGES;
+use pdm_api_types::PRIVILEGES;
use server::api;
@@ -45,12 +43,10 @@ fn main() -> Result<(), Error> {
fn generate_api_tree() -> String {
let mut tree = Vec::new();
- let mut data = dump_api_schema(&api::ROUTER, ".");
+ let mut data = proxmox_docgen::generate_api_tree(&api::ROUTER, ".", PRIVILEGES);
data["path"] = "/".into();
// hack: add invisible space to sort as first entry
data["text"] = "​Management API (HTTP)".into();
- data["expanded"] = true.into();
-
tree.push(data);
format!(
@@ -58,305 +54,3 @@ fn generate_api_tree() -> String {
serde_json::to_string_pretty(&tree).unwrap()
)
}
-
-pub fn dump_schema(schema: &Schema) -> Value {
- let mut data;
-
- match schema {
- Schema::Null => {
- data = json!({
- "type": "null",
- });
- }
- Schema::Boolean(boolean_schema) => {
- data = json!({
- "type": "boolean",
- "description": boolean_schema.description,
- });
- if let Some(default) = boolean_schema.default {
- data["default"] = default.into();
- }
- }
- Schema::String(string_schema) => {
- data = json!({
- "type": "string",
- "description": string_schema.description,
- });
- if let Some(default) = string_schema.default {
- data["default"] = default.into();
- }
- if let Some(min_length) = string_schema.min_length {
- data["minLength"] = min_length.into();
- }
- if let Some(max_length) = string_schema.max_length {
- data["maxLength"] = max_length.into();
- }
- if let Some(type_text) = string_schema.type_text {
- data["typetext"] = type_text.into();
- }
- match string_schema.format {
- None | Some(ApiStringFormat::VerifyFn(_)) => { /* do nothing */ }
- Some(ApiStringFormat::Pattern(const_regex)) => {
- data["pattern"] = format!("/{}/", const_regex.regex_string).into();
- }
- Some(ApiStringFormat::Enum(variants)) => {
- let variants: Vec<String> =
- variants.iter().map(|e| e.value.to_string()).collect();
- data["enum"] = serde_json::to_value(variants).unwrap();
- }
- Some(ApiStringFormat::PropertyString(subschema)) => {
- match subschema {
- Schema::Object(_) | Schema::Array(_) => {
- data["format"] = dump_schema(subschema);
- data["typetext"] = get_property_string_type_text(subschema).into();
- }
- _ => { /* do nothing - should not happen */ }
- };
- }
- }
- // fixme: dump format
- }
- Schema::Integer(integer_schema) => {
- data = json!({
- "type": "integer",
- "description": integer_schema.description,
- });
- if let Some(default) = integer_schema.default {
- data["default"] = default.into();
- }
- if let Some(minimum) = integer_schema.minimum {
- data["minimum"] = minimum.into();
- }
- if let Some(maximum) = integer_schema.maximum {
- data["maximum"] = maximum.into();
- }
- }
- Schema::Number(number_schema) => {
- data = json!({
- "type": "number",
- "description": number_schema.description,
- });
- if let Some(default) = number_schema.default {
- data["default"] = default.into();
- }
- if let Some(minimum) = number_schema.minimum {
- data["minimum"] = minimum.into();
- }
- if let Some(maximum) = number_schema.maximum {
- data["maximum"] = maximum.into();
- }
- }
- Schema::Object(object_schema) => {
- data = dump_property_schema(object_schema);
- data["type"] = "object".into();
- if let Some(default_key) = object_schema.default_key {
- data["default_key"] = default_key.into();
- }
- }
- Schema::Array(array_schema) => {
- data = json!({
- "type": "array",
- "description": array_schema.description,
- "items": dump_schema(array_schema.items),
- });
- if let Some(min_length) = array_schema.min_length {
- data["minLength"] = min_length.into();
- }
- if let Some(max_length) = array_schema.min_length {
- data["maxLength"] = max_length.into();
- }
- }
- Schema::AllOf(alloff_schema) => {
- data = dump_property_schema(alloff_schema);
- data["type"] = "object".into();
- }
- Schema::OneOf(schema) => {
- let mut type_schema = dump_schema(schema.type_schema());
- if schema.type_property_entry.1 {
- type_schema["optional"] = true.into();
- }
- data = json!({
- "type": "object",
- "description": schema.description,
- "typeProperty": schema.type_property(),
- "typeSchema": type_schema,
- });
- let mut variants = Vec::with_capacity(schema.list.len());
- for (title, variant) in schema.list {
- let mut entry = dump_schema(variant);
- entry["title"] = (*title).into();
- variants.push(entry);
- }
- data["oneOf"] = variants.into();
- }
- };
-
- data
-}
-
-pub fn dump_property_schema(param: &dyn ObjectSchemaType) -> Value {
- let mut properties = json!({});
-
- for (prop, optional, schema) in param.properties() {
- let mut property = dump_schema(schema);
- if *optional {
- property["optional"] = 1.into();
- }
- properties[prop] = property;
- }
-
- let data = json!({
- "description": param.description(),
- "additionalProperties": param.additional_properties(),
- "properties": properties,
- });
-
- data
-}
-
-fn dump_api_permission(permission: &Permission) -> Value {
- match permission {
- Permission::Superuser => json!({ "user": "root@pam" }),
- Permission::User(user) => json!({ "user": user }),
- Permission::Anybody => json!({ "user": "all" }),
- Permission::World => json!({ "user": "world" }),
- Permission::UserParam(param) => json!({ "userParam": param }),
- Permission::Group(group) => json!({ "group": group }),
- Permission::WithParam(param, sub_permission) => {
- json!({
- "withParam": {
- "name": param,
- "permissions": dump_api_permission(sub_permission),
- },
- })
- }
- Permission::Privilege(name, value, partial) => {
- let mut privs = Vec::new();
- for (name, v) in PRIVILEGES {
- if (value & v) != 0 {
- privs.push(name.to_string());
- }
- }
-
- json!({
- "check": {
- "path": name,
- "privs": privs,
- "partial": partial,
- }
- })
- }
- Permission::And(list) => {
- let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
- json!({ "and": list })
- }
- Permission::Or(list) => {
- let list: Vec<Value> = list.iter().map(|p| dump_api_permission(p)).collect();
- json!({ "or": list })
- }
- }
-}
-
-fn dump_api_method_schema(method: &str, api_method: &ApiMethod) -> Value {
- let mut data = json!({
- "description": api_method.parameters.description(),
- });
-
- data["parameters"] = dump_property_schema(&api_method.parameters);
-
- let mut returns = dump_schema(api_method.returns.schema);
- if api_method.returns.optional {
- returns["optional"] = 1.into();
- }
- data["returns"] = returns;
-
- match api_method.access {
- ApiAccess {
- description: None,
- permission: Permission::Superuser,
- } => {
- // no need to output default
- }
- ApiAccess {
- description,
- permission,
- } => {
- let mut permissions = dump_api_permission(permission);
- if let Some(description) = description {
- permissions["description"] = description.into();
- }
- data["permissions"] = permissions;
- }
- }
-
- let mut method = method;
-
- if let ApiHandler::AsyncHttp(_) = api_method.handler {
- method = if method == "POST" { "UPLOAD" } else { method };
- method = if method == "GET" { "DOWNLOAD" } else { method };
- }
-
- data["method"] = method.into();
-
- data
-}
-
-pub fn dump_api_schema(router: &Router, path: &str) -> Value {
- let mut data = json!({});
-
- let mut info = json!({});
- if let Some(api_method) = router.get {
- info["GET"] = dump_api_method_schema("GET", api_method);
- }
- if let Some(api_method) = router.post {
- info["POST"] = dump_api_method_schema("POST", api_method);
- }
- if let Some(api_method) = router.put {
- info["PUT"] = dump_api_method_schema("PUT", api_method);
- }
- if let Some(api_method) = router.delete {
- info["DELETE"] = dump_api_method_schema("DELETE", api_method);
- }
-
- data["info"] = info;
-
- match &router.subroute {
- None => {
- data["leaf"] = 1.into();
- }
- Some(SubRoute::MatchAll { router, param_name }) => {
- let sub_path = if path == "." {
- format!("/{{{}}}", param_name)
- } else {
- format!("{}/{{{}}}", path, param_name)
- };
- let mut child = dump_api_schema(router, &sub_path);
- child["path"] = sub_path.into();
- child["text"] = format!("{{{}}}", param_name).into();
-
- let children = vec![child];
- data["children"] = children.into();
- data["leaf"] = 0.into();
- }
- Some(SubRoute::Map(dirmap)) => {
- let mut children = Vec::new();
-
- for (key, sub_router) in dirmap.iter() {
- let sub_path = if path == "." {
- format!("/{}", key)
- } else {
- format!("{}/{}", path, key)
- };
- let mut child = dump_api_schema(sub_router, &sub_path);
- child["path"] = sub_path.into();
- child["text"] = key.to_string().into();
- children.push(child);
- }
-
- data["children"] = children.into();
- data["leaf"] = 0.into();
- }
- }
-
- data
-}
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread* [pdm-devel] [PATCH datacenter-manager v3 2/2] api-viewer: add an api-viewer package
2025-11-19 14:05 [pdm-devel] [PATCH datacenter-manager/proxmox{, -backup}/widget-toolkit v3 00/10] unstable flag and pdm api viewer Shannon Sterz
` (8 preceding siblings ...)
2025-11-19 14:06 ` [pdm-devel] [PATCH datacenter-manager v3 1/2] docgen: switch to proxmox-rs docgen crate Shannon Sterz
@ 2025-11-19 14:06 ` Shannon Sterz
9 siblings, 0 replies; 11+ messages in thread
From: Shannon Sterz @ 2025-11-19 14:06 UTC (permalink / raw)
To: pdm-devel
this can be installed as a suggested package and will then be served
at `https://$pdm-host::8443/docs/api-viewer/apidoc.js`.
Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
---
Makefile | 1 +
debian/control | 13 +++++++++
...xmox-datacenter-manager-api-viewer.install | 1 +
docs/api-viewer/Makefile | 27 +++++++++++++++++++
docs/api-viewer/index.html | 16 +++++++++++
5 files changed, 58 insertions(+)
create mode 100644 debian/proxmox-datacenter-manager-api-viewer.install
create mode 100644 docs/api-viewer/Makefile
create mode 100644 docs/api-viewer/index.html
diff --git a/Makefile b/Makefile
index b2f794a..ca721eb 100644
--- a/Makefile
+++ b/Makefile
@@ -82,6 +82,7 @@ install: $(COMPILED_BINS) $(SHELL_COMPLETION_FILES)
$(foreach i,$(ZSH_COMPLETIONS), \
install -m644 $(COMPLETION_DIR)/$(i) $(DESTDIR)$(ZSHCOMPDIR)/ ;)
make -C services install
+ make -C docs/api-viewer install
$(COMPILED_BINS): .do-cargo-build
.do-cargo-build:
diff --git a/debian/control b/debian/control
index 65700ae..ab813df 100644
--- a/debian/control
+++ b/debian/control
@@ -125,6 +125,7 @@ Build-Depends: cargo:native,
librust-zstd-0.13+default-dev,
libstd-rust-dev,
libsystemd-dev,
+ proxmox-widget-toolkit-dev,
rustc:native,
Maintainer: Proxmox Support Team <support@proxmox.com>
Standards-Version: 4.6.1
@@ -143,6 +144,7 @@ Depends: proxmox-mini-journalreader,
${misc:Depends},
${shlibs:Depends},
Recommends: proxmox-datacenter-manager-client, proxmox-datacenter-manager-ui,
+Suggests: proxmox-datacenter-manager-api-viewer,
Description: Manage multiple Proxmox VE cluster and other Proxmox projects
This package provides the API daemons of the Proxmox Datacenter Manager (PDM)
which allows one to add multiple Proxmox VE and Proxmox Backup Server
@@ -155,3 +157,14 @@ Depends: ${misc:Depends}, ${shlibs:Depends},
Description: CLI Client for the Proxmox Datacenter Manager
This package provides the CLI client that can interface with a Proxmox
Datacenter Manager (PDM) instance.
+
+Package: proxmox-datacenter-manager-api-viewer
+Architecture: any
+Multi-Arch: allowed
+Depends: fonts-font-awesome,
+ libjs-extjs (>= 7~),
+ proxmox-widget-toolkit,
+ ${misc:Depends},
+ ${shlibs:Depends},
+Recommends: proxmox-datacenter-manager,
+Description: API Viewer for the Proxmox Datacenter Manager.
diff --git a/debian/proxmox-datacenter-manager-api-viewer.install b/debian/proxmox-datacenter-manager-api-viewer.install
new file mode 100644
index 0000000..0e4d9df
--- /dev/null
+++ b/debian/proxmox-datacenter-manager-api-viewer.install
@@ -0,0 +1 @@
+usr/share/doc/proxmox-datacenter-manager/html
diff --git a/docs/api-viewer/Makefile b/docs/api-viewer/Makefile
new file mode 100644
index 0000000..c64bfbb
--- /dev/null
+++ b/docs/api-viewer/Makefile
@@ -0,0 +1,27 @@
+DOCDIR = /usr/share/doc/proxmox-datacenter-manager
+
+API_VIEWER_SOURCES= \
+ index.html \
+ apidoc.js
+
+API_VIEWER_FILES := \
+ apidata.js \
+ /usr/share/javascript/proxmox-widget-toolkit-dev/APIViewer.js
+
+ifeq ($(BUILD_MODE), release)
+COMPILEDIR := ../../target/release
+else
+COMPILEDIR := ../../target/debug
+endif
+
+.PHONY: install
+install: ${API_VIEWER_SOURCES}
+ install -dm 0755 $(DESTDIR)$(DOCDIR)/html/api-viewer
+ install -m 0644 ${API_VIEWER_SOURCES} $(DESTDIR)$(DOCDIR)/html/api-viewer
+
+apidata.js: ${COMPILEDIR}/docgen
+ ${COMPILEDIR}/docgen apidata.js >$@
+
+apidoc.js: ${API_VIEWER_FILES}
+ cat ${API_VIEWER_FILES} >$@.tmp
+ mv $@.tmp $@
diff --git a/docs/api-viewer/index.html b/docs/api-viewer/index.html
new file mode 100644
index 0000000..2bf97c5
--- /dev/null
+++ b/docs/api-viewer/index.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+ <title>Proxmox Datacenter Manager API Documentation</title>
+
+ <link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css">
+ <link rel="stylesheet" type="text/css" href="/proxmox-extjs-widget-toolkit/css/ext6-pmx.css" />
+ <link rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" href="/proxmox-extjs-widget-toolkit/themes/theme-proxmox-dark.css" />
+ <link rel="stylesheet" type="text/css" href="/fontawesome/css/font-awesome.css" />
+ <script type="text/javascript" src="/extjs/ext-all.js"></script>
+ <script type="text/javascript" src="apidoc.js"></script>
+</head>
+<body></body>
+</html>
--
2.47.3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread