* [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view
@ 2025-09-02 15:14 Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH manager 1/1] api: apt: add JSON schema for 'list_updates' endpoint Lukas Wagner
` (11 more replies)
0 siblings, 12 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
This series adds a new 'Updates' tab for PVE remotes. The existing status
overview is moved to a new 'Overview' tab, which is visible by default.
On the backend side, we add a couple new API endpoints, which simply pass
through the request to the PVE nodes, no caching for now.
GET /pve/remotes/{remote}/nodes/{node}/apt
Get list of updatable packages
GET /pve/remotes/{remote}/nodes/{node}/changelog
Get list of changelog of package
POST /pve/remotes/{remote}/nodes/{node}/apt
Update APT package database
In terms of permissions, these new API endpoints require RESOURCE_MODIFY privs on
/resource/{remote}/node/{node}/system
This was the result of a short discussion in the development chat room.
The existing APT view component is a bit large for this panel, maybe we could
hide the package description by default (but not too important for now).
Future work (some backend work already started, but can't finish before my
vacation):
- "Global Update" view that lists update status of all remote nodes
- Cache update status per node (absolutely necessary for the 'global' view),
with a task refreshing the update status every couple of hours
- Maybe send a notification about the global update availabilty (require notification
stack integration first)
- Add new API functions to pdm-client crate and CLI
- Allow package upgrade (requires web socket proxying, as far as I can see,
haven't really looked into it much)
pve-manager:
Lukas Wagner (1):
api: apt: add JSON schema for 'list_updates' endpoint
PVE/API2/APT.pm | 46 +++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 45 insertions(+), 1 deletion(-)
proxmox-api-types:
Lukas Wagner (3):
Schema2Rust: fix handling of non-optional params
generate: add bindings for various APT functions
refresh bindings
pve-api-types/generate.pl | 4 +
pve-api-types/generator-lib/Schema2Rust.pm | 9 +-
pve-api-types/src/generated/code.rs | 58 ++++++++++-
pve-api-types/src/generated/types.rs | 108 +++++++++++++++++++++
4 files changed, 175 insertions(+), 4 deletions(-)
proxmox-yew-comp:
Lukas Wagner (2):
apt view: allow to set task_base_url
apt view: reload if base urls have changed
src/apt_package_manager.rs | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
proxmox-datacenter-manager:
Lukas Wagner (4):
server: add api for getting available updates/changelogs for remote
nodes
ui: pve: promote node.rs to dir-style module
ui: pve: move node overview to a new overview tab
ui: pve: node: add update tab
server/src/api/pve/apt.rs | 119 +++++++++++++++++++++++
server/src/api/pve/mod.rs | 3 +-
server/src/api/pve/node.rs | 1 +
server/src/lib.rs | 1 +
server/src/remote_updates.rs | 96 ++++++++++++++++++
ui/src/pve/node/mod.rs | 103 ++++++++++++++++++++
ui/src/pve/{node.rs => node/overview.rs} | 31 +++---
7 files changed, 333 insertions(+), 21 deletions(-)
create mode 100644 server/src/api/pve/apt.rs
create mode 100644 server/src/remote_updates.rs
create mode 100644 ui/src/pve/node/mod.rs
rename ui/src/pve/{node.rs => node/overview.rs} (95%)
Summary over all repositories:
13 files changed, 576 insertions(+), 26 deletions(-)
--
Generated by murpp 0.9.0
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH manager 1/1] api: apt: add JSON schema for 'list_updates' endpoint
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-03 8:25 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 1/3] Schema2Rust: fix handling of non-optional params Lukas Wagner
` (10 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
This will be shown in the API viewer. Additionally, this allows us to
automatically generate the appropriate Rust API type automatically.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
PVE/API2/APT.pm | 46 +++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 45 insertions(+), 1 deletion(-)
diff --git a/PVE/API2/APT.pm b/PVE/API2/APT.pm
index bd651ba1..990c1155 100644
--- a/PVE/API2/APT.pm
+++ b/PVE/API2/APT.pm
@@ -219,7 +219,51 @@ __PACKAGE__->register_method({
type => "array",
items => {
type => "object",
- properties => {},
+ properties => {
+ 'Arch' => {
+ type => 'string',
+ description => 'Package Architecture.',
+ },
+ 'Description' => {
+ type => 'string',
+ description => 'Human-readable package description.',
+ },
+ 'NotifyStatus' => {
+ type => 'string',
+ description =>
+ 'Version for which PVE has already sent an update notification for.',
+ optional => 1,
+ },
+ 'OldVersion' => {
+ type => 'string',
+ description => 'Old version currently installed.',
+ optional => 1,
+ },
+ 'Origin' => {
+ type => 'string',
+ description => 'Package origin.',
+ },
+ 'Package' => {
+ type => 'string',
+ description => 'Package name.',
+ },
+ 'Priority' => {
+ type => 'string',
+ description => 'Package priority in human-readable form.',
+ },
+ 'Section' => {
+ type => 'string',
+ description => 'Package section.',
+ },
+ 'Title' => {
+ type => 'string',
+ description => 'Package title.',
+ },
+ 'Version' => {
+ type => 'string',
+ description => 'New version to be updated to.',
+ },
+ },
},
},
code => sub {
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-api-types 1/3] Schema2Rust: fix handling of non-optional params
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH manager 1/1] api: apt: add JSON schema for 'list_updates' endpoint Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-03 8:57 ` [pdm-devel] applied: " Wolfgang Bumiller
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 2/3] generate: add bindings for various APT functions Lukas Wagner
` (9 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
pve-api-types/generator-lib/Schema2Rust.pm | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/pve-api-types/generator-lib/Schema2Rust.pm b/pve-api-types/generator-lib/Schema2Rust.pm
index 77cb373..436ba76 100644
--- a/pve-api-types/generator-lib/Schema2Rust.pm
+++ b/pve-api-types/generator-lib/Schema2Rust.pm
@@ -497,8 +497,10 @@ my sub print_method_without_body : prototype($$$$$) {
print {$out} " .maybe_bool_arg(\"$name\", p_$rust_name)\n";
} elsif ($arg->{is_string_list}) {
print {$out} " .maybe_list_arg(\"$name\", p_$rust_name)\n";
- } else {
+ } elsif ($arg->{optional}) {
print {$out} " .maybe_arg(\"$name\", &p_$rust_name)\n";
+ } else {
+ print {$out} " .arg(\"$name\", &p_$rust_name)\n";
}
}
print {$out} " .build();\n";
@@ -517,12 +519,15 @@ my sub print_method_without_body : prototype($$$$$) {
for my $arg (@$input) {
my $name = $arg->{name};
my $rust_name = $arg->{rust_name};
+
if ($arg->{type} eq 'Option<bool>') {
print {$out} " .maybe_bool_arg(\"$name\", $rust_name)\n";
} elsif ($arg->{is_string_list}) {
print {$out} " .maybe_list_arg(\"$name\", &$rust_name)\n";
- } else {
+ } elsif ($arg->{optional}) {
print {$out} " .maybe_arg(\"$name\", &$rust_name)\n";
+ } else {
+ print {$out} " .arg(\"$name\", &$rust_name)\n";
}
}
print {$out} " .build();\n";
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-api-types 2/3] generate: add bindings for various APT functions
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH manager 1/1] api: apt: add JSON schema for 'list_updates' endpoint Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 1/3] Schema2Rust: fix handling of non-optional params Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-03 8:58 ` [pdm-devel] applied: " Wolfgang Bumiller
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 3/3] refresh bindings Lukas Wagner
` (8 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
This adds bindings for
GET /nodes/<node>/apt/update
POST /nodes/<node>/apt/update
GET /nodes/<node>/apt/changelog
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
pve-api-types/generate.pl | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
index 966c4d0..c3bece0 100644
--- a/pve-api-types/generate.pl
+++ b/pve-api-types/generate.pl
@@ -330,6 +330,10 @@ Schema2Rust::derive('ListRealm' => 'Clone', 'PartialEq');
api(POST => '/access/users/{userid}/token/{tokenid}', 'create_token', 'param-name' => 'CreateToken');
Schema2Rust::derive('CreateToken' => 'Default');
+api(GET => '/nodes/{node}/apt/update', 'list_available_updates', 'return-name' => 'AptUpdateInfo');
+api(POST => '/nodes/{node}/apt/update', 'update_apt_database', 'output-type' => 'PveUpid', 'param-name' => 'AptUpdateParams');
+api(GET => '/nodes/{node}/apt/changelog', 'get_package_changelog', 'output-type' => 'String');
+
# NOW DUMP THE CODE:
#
# We generate one file for API types, and one for API method calls.
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-api-types 3/3] refresh bindings
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (2 preceding siblings ...)
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 2/3] generate: add bindings for various APT functions Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-03 8:59 ` Wolfgang Bumiller
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-yew-comp 1/2] apt view: allow to set task_base_url Lukas Wagner
` (7 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
pve-api-types/src/generated/code.rs | 58 +++++++++++++-
pve-api-types/src/generated/types.rs | 108 +++++++++++++++++++++++++++
2 files changed, 164 insertions(+), 2 deletions(-)
diff --git a/pve-api-types/src/generated/code.rs b/pve-api-types/src/generated/code.rs
index 1f34347..03ddcaa 100644
--- a/pve-api-types/src/generated/code.rs
+++ b/pve-api-types/src/generated/code.rs
@@ -144,9 +144,7 @@
/// - /nodes/{node}
/// - /nodes/{node}/aplinfo
/// - /nodes/{node}/apt
-/// - /nodes/{node}/apt/changelog
/// - /nodes/{node}/apt/repositories
-/// - /nodes/{node}/apt/update
/// - /nodes/{node}/apt/versions
/// - /nodes/{node}/capabilities
/// - /nodes/{node}/capabilities/qemu
@@ -424,6 +422,16 @@ pub trait PveClient {
Err(Error::Other("create_token not implemented"))
}
+ /// Get package changelogs.
+ async fn get_package_changelog(
+ &self,
+ node: &str,
+ name: String,
+ version: Option<String>,
+ ) -> Result<String, Error> {
+ Err(Error::Other("get_package_changelog not implemented"))
+ }
+
/// Read subscription info.
async fn get_subscription(&self, node: &str) -> Result<NodeSubscriptionInfo, Error> {
Err(Error::Other("get_subscription not implemented"))
@@ -455,6 +463,11 @@ pub trait PveClient {
Err(Error::Other("get_task_status not implemented"))
}
+ /// List available updates.
+ async fn list_available_updates(&self, node: &str) -> Result<Vec<AptUpdateInfo>, Error> {
+ Err(Error::Other("list_available_updates not implemented"))
+ }
+
/// Authentication domain index.
async fn list_domains(&self) -> Result<Vec<ListRealm>, Error> {
Err(Error::Other("list_domains not implemented"))
@@ -659,6 +672,16 @@ pub trait PveClient {
Err(Error::Other("stop_task not implemented"))
}
+ /// This is used to resynchronize the package index files from their sources
+ /// (apt-get update).
+ async fn update_apt_database(
+ &self,
+ node: &str,
+ params: AptUpdateParams,
+ ) -> Result<PveUpid, Error> {
+ Err(Error::Other("update_apt_database not implemented"))
+ }
+
/// API version details, including some parts of the global datacenter
/// config.
async fn version(&self) -> Result<VersionResponse, Error> {
@@ -724,6 +747,20 @@ where
Ok(self.0.post(url, ¶ms).await?.expect_json()?.data)
}
+ /// Get package changelogs.
+ async fn get_package_changelog(
+ &self,
+ node: &str,
+ name: String,
+ version: Option<String>,
+ ) -> Result<String, Error> {
+ let url = &ApiPathBuilder::new(format!("/api2/extjs/nodes/{node}/apt/changelog"))
+ .arg("name", &name)
+ .maybe_arg("version", &version)
+ .build();
+ Ok(self.0.get(url).await?.expect_json()?.data)
+ }
+
/// Read subscription info.
async fn get_subscription(&self, node: &str) -> Result<NodeSubscriptionInfo, Error> {
let url = &format!("/api2/extjs/nodes/{node}/subscription");
@@ -787,6 +824,12 @@ where
Ok(self.0.get(url).await?.expect_json()?.data)
}
+ /// List available updates.
+ async fn list_available_updates(&self, node: &str) -> Result<Vec<AptUpdateInfo>, Error> {
+ let url = &format!("/api2/extjs/nodes/{node}/apt/update");
+ Ok(self.0.get(url).await?.expect_json()?.data)
+ }
+
/// Authentication domain index.
async fn list_domains(&self) -> Result<Vec<ListRealm>, Error> {
let url = "/api2/extjs/access/domains";
@@ -1032,6 +1075,17 @@ where
self.0.delete(url).await?.nodata()
}
+ /// This is used to resynchronize the package index files from their sources
+ /// (apt-get update).
+ async fn update_apt_database(
+ &self,
+ node: &str,
+ params: AptUpdateParams,
+ ) -> Result<PveUpid, Error> {
+ let url = &format!("/api2/extjs/nodes/{node}/apt/update");
+ Ok(self.0.post(url, ¶ms).await?.expect_json()?.data)
+ }
+
/// API version details, including some parts of the global datacenter
/// config.
async fn version(&self) -> Result<VersionResponse, Error> {
diff --git a/pve-api-types/src/generated/types.rs b/pve-api-types/src/generated/types.rs
index 35fa6cc..70b7b30 100644
--- a/pve-api-types/src/generated/types.rs
+++ b/pve-api-types/src/generated/types.rs
@@ -1,3 +1,111 @@
+#[api(
+ properties: {
+ Arch: {
+ type: String,
+ },
+ Description: {
+ type: String,
+ },
+ NotifyStatus: {
+ optional: true,
+ type: String,
+ },
+ OldVersion: {
+ optional: true,
+ type: String,
+ },
+ Origin: {
+ type: String,
+ },
+ Package: {
+ type: String,
+ },
+ Priority: {
+ type: String,
+ },
+ Section: {
+ type: String,
+ },
+ Title: {
+ type: String,
+ },
+ Version: {
+ type: String,
+ },
+ },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct AptUpdateInfo {
+ /// Package Architecture.
+ #[serde(rename = "Arch")]
+ pub arch: String,
+
+ /// Human-readable package description.
+ #[serde(rename = "Description")]
+ pub description: String,
+
+ /// Version for which PVE has already sent a update notification for.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[serde(rename = "NotifyStatus")]
+ pub notify_status: Option<String>,
+
+ /// Old version currently installed.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ #[serde(rename = "OldVersion")]
+ pub old_version: Option<String>,
+
+ /// Package origin.
+ #[serde(rename = "Origin")]
+ pub origin: String,
+
+ /// Package name.
+ #[serde(rename = "Package")]
+ pub package: String,
+
+ /// Package priority in human-readable form.
+ #[serde(rename = "Priority")]
+ pub priority: String,
+
+ /// Package section.
+ #[serde(rename = "Section")]
+ pub section: String,
+
+ /// Package title.
+ #[serde(rename = "Title")]
+ pub title: String,
+
+ /// New version to be updated to.
+ #[serde(rename = "Version")]
+ pub version: String,
+}
+
+#[api(
+ properties: {
+ notify: {
+ default: false,
+ optional: true,
+ },
+ quiet: {
+ default: false,
+ optional: true,
+ },
+ },
+)]
+/// Object.
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct AptUpdateParams {
+ /// Send notification about new packages.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub notify: Option<bool>,
+
+ /// Only produces output suitable for logging, omitting progress indicators.
+ #[serde(deserialize_with = "proxmox_serde::perl::deserialize_bool")]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub quiet: Option<bool>,
+}
+
const CLUSTER_RESOURCE_CONTENT: Schema =
proxmox_schema::ArraySchema::new("list", &StorageContent::API_SCHEMA).schema();
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-yew-comp 1/2] apt view: allow to set task_base_url
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (3 preceding siblings ...)
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 3/3] refresh bindings Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-yew-comp 2/2] apt view: reload if base urls have changed Lukas Wagner
` (6 subsequent siblings)
11 siblings, 0 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
This one is used to poll the 'apt update' task progress. If we want to
use this component for *remote* nodes, we need to be able to set a
custom base url for the task API.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
src/apt_package_manager.rs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/apt_package_manager.rs b/src/apt_package_manager.rs
index 282caa9..6cd179f 100644
--- a/src/apt_package_manager.rs
+++ b/src/apt_package_manager.rs
@@ -40,6 +40,11 @@ pub struct AptPackageManager {
/// The base url for
pub base_url: AttrValue,
+ #[prop_or("/nodes/localhost/tasks".into())]
+ #[builder(IntoPropValue, into_prop_value)]
+ /// The base url for tasks
+ pub task_base_url: AttrValue,
+
/// Enable the upgrade button
#[prop_or_default]
#[builder]
@@ -193,6 +198,9 @@ impl LoadableComponent for ProxmoxAptPackageManager {
.class("pwt-border-bottom")
.with_child(Button::new(tr!("Refresh")).onclick({
let link = ctx.link();
+
+ link.task_base_url(props.task_base_url.clone());
+
let command = format!("{}/update", props.base_url);
move |_| link.start_task(&command, None, false)
}))
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-yew-comp 2/2] apt view: reload if base urls have changed
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (4 preceding siblings ...)
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-yew-comp 1/2] apt view: allow to set task_base_url Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes Lukas Wagner
` (5 subsequent siblings)
11 siblings, 0 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
If we use this view to show available node updates, we might want to
change the node afterwards (e.g. by selecting another node in a tree).
By implementing the `changed` function we can trigger a reload if the
relevant props change.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
src/apt_package_manager.rs | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/src/apt_package_manager.rs b/src/apt_package_manager.rs
index 6cd179f..3b871f7 100644
--- a/src/apt_package_manager.rs
+++ b/src/apt_package_manager.rs
@@ -256,6 +256,21 @@ impl LoadableComponent for ProxmoxAptPackageManager {
}
}
}
+
+ fn changed(
+ &mut self,
+ ctx: &LoadableComponentContext<Self>,
+ old_props: &Self::Properties,
+ ) -> bool {
+ let props = ctx.props();
+
+ if props.base_url != old_props.base_url || props.task_base_url != old_props.task_base_url {
+ ctx.link().send_reload();
+ true
+ } else {
+ false
+ }
+ }
}
impl From<AptPackageManager> for VNode {
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (5 preceding siblings ...)
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-yew-comp 2/2] apt view: reload if base urls have changed Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-03 8:42 ` Lukas Wagner
2025-09-03 9:02 ` Stefan Hanreich
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/4] ui: pve: promote node.rs to dir-style module Lukas Wagner
` (4 subsequent siblings)
11 siblings, 2 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
This adds new APIs for update management:
GET /pve/remotes/{remote}/nodes/{node}/apt/changelog
-> get package changelog
GET /pve/remotes/{remote}/nodes/{node}/apt/update
-> get list of updatable packages
POST /pve/remotes/{remote}/nodes/{node}/apt/update
-> refresh APT database
At this time these just pass the call through to PVE with no caching
involved on the PDM side. This should be fine for this API, but once
we have an API for 'give me a view of ALL available remote updates',
we need to introduce a cache that is periodically refreshed.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
server/src/api/pve/apt.rs | 119 +++++++++++++++++++++++++++++++++++
server/src/api/pve/mod.rs | 3 +-
server/src/api/pve/node.rs | 1 +
server/src/lib.rs | 1 +
server/src/remote_updates.rs | 96 ++++++++++++++++++++++++++++
5 files changed, 219 insertions(+), 1 deletion(-)
create mode 100644 server/src/api/pve/apt.rs
create mode 100644 server/src/remote_updates.rs
diff --git a/server/src/api/pve/apt.rs b/server/src/api/pve/apt.rs
new file mode 100644
index 00000000..f5027fb8
--- /dev/null
+++ b/server/src/api/pve/apt.rs
@@ -0,0 +1,119 @@
+use anyhow::Error;
+
+use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
+use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
+use proxmox_schema::api;
+use proxmox_schema::api_types::NODE_SCHEMA;
+
+use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, RemoteUpid, PRIV_RESOURCE_MODIFY};
+
+use crate::{api::remotes::get_remote, remote_updates};
+
+#[api(
+ input: {
+ properties: {
+ remote: {
+ schema: REMOTE_ID_SCHEMA,
+ },
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ returns: {
+ description: "A list of packages with available updates.",
+ type: Array,
+ items: {
+ type: APTUpdateInfo
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+ },
+)]
+/// List available APT updates for a remote PVE node.
+async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
+ let (config, _digest) = pdm_config::remotes::config()?;
+ let remote = get_remote(&config, &remote)?;
+
+ let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
+
+ Ok(updates)
+}
+
+#[api(
+ input: {
+ properties: {
+ remote: {
+ schema: REMOTE_ID_SCHEMA,
+ },
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ },
+ },
+ access: {
+ permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+ },
+)]
+/// Update the APT database of a remote PVE node.
+pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
+ let (config, _digest) = pdm_config::remotes::config()?;
+ let remote = get_remote(&config, &remote)?;
+
+ let upid = remote_updates::update_apt_database(remote, &node).await?;
+
+ Ok(upid)
+}
+
+#[api(
+ input: {
+ properties: {
+ remote: {
+ schema: REMOTE_ID_SCHEMA,
+ },
+ node: {
+ schema: NODE_SCHEMA,
+ },
+ options: {
+ type: APTGetChangelogOptions,
+ flatten: true,
+ },
+ },
+ },
+ returns: {
+ description: "The Package changelog.",
+ type: String,
+ },
+ access: {
+ permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
+ },
+)]
+/// Retrieve the changelog of the specified package for a remote PVE node.
+async fn apt_get_changelog(
+ remote: String,
+ node: String,
+ options: APTGetChangelogOptions,
+) -> Result<String, Error> {
+ let (config, _digest) = pdm_config::remotes::config()?;
+ let remote = get_remote(&config, &remote)?;
+
+ remote_updates::get_changelog(remote.clone(), &node, options.name).await
+}
+
+const SUBDIRS: SubdirMap = &[
+ (
+ "changelog",
+ &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
+ ),
+ (
+ "update",
+ &Router::new()
+ .get(&API_METHOD_APT_UPDATE_AVAILABLE)
+ .post(&API_METHOD_APT_UPDATE_DATABASE),
+ ),
+];
+
+pub const ROUTER: Router = Router::new()
+ .get(&list_subdirs_api_method!(SUBDIRS))
+ .subdirs(SUBDIRS);
diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
index 2cfdc5b7..0768083d 100644
--- a/server/src/api/pve/mod.rs
+++ b/server/src/api/pve/mod.rs
@@ -31,6 +31,7 @@ use crate::connection::PveClient;
use crate::connection::{self, probe_tls_connection};
use crate::remote_tasks;
+mod apt;
mod lxc;
mod node;
mod qemu;
@@ -77,7 +78,7 @@ const RESOURCES_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_RESOURCES
const STATUS_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_STATUS);
// converts a remote + PveUpid into a RemoteUpid and starts tracking it
-async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
+pub async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
remote_tasks::track_running_task(remote_upid.clone()).await?;
Ok(remote_upid)
diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
index df96a1c3..99539d1c 100644
--- a/server/src/api/pve/node.rs
+++ b/server/src/api/pve/node.rs
@@ -13,6 +13,7 @@ pub const ROUTER: Router = Router::new()
#[sortable]
const SUBDIRS: SubdirMap = &sorted!([
+ ("apt", &super::apt::ROUTER),
("rrddata", &super::rrddata::NODE_RRD_ROUTER),
("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
("storage", &Router::new().get(&API_METHOD_GET_STORAGES)),
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 3f8b7708..a58190d8 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -9,6 +9,7 @@ pub mod metric_collection;
pub mod parallel_fetcher;
pub mod remote_cache;
pub mod remote_tasks;
+pub mod remote_updates;
pub mod resource_cache;
pub mod task_utils;
diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
new file mode 100644
index 00000000..809e5de1
--- /dev/null
+++ b/server/src/remote_updates.rs
@@ -0,0 +1,96 @@
+use anyhow::Error;
+use pdm_api_types::RemoteUpid;
+
+use proxmox_apt_api_types::APTUpdateInfo;
+
+use pdm_api_types::remotes::{Remote, RemoteType};
+
+use crate::api::pve::new_remote_upid;
+use crate::connection;
+
+/// Return a list of available updates for a given remote node.
+pub async fn list_available_updates(
+ remote: Remote,
+ node: &str,
+) -> Result<Vec<APTUpdateInfo>, Error> {
+ let updates = fetch_available_updates(remote, node.to_string()).await?;
+ Ok(updates)
+}
+
+/// Trigger `apt update` on a remote node.
+///
+/// The function returns a `[RemoteUpid]` for the started update task.
+pub async fn update_apt_database(remote: &Remote, node: &str) -> Result<RemoteUpid, Error> {
+ match remote.ty {
+ RemoteType::Pve => {
+ let client = connection::make_pve_client(remote)?;
+
+ let params = pve_api_types::AptUpdateParams {
+ notify: Some(false),
+ quiet: Some(false),
+ };
+ let upid = client.update_apt_database(node, params).await?;
+
+ new_remote_upid(remote.id.clone(), upid).await
+ }
+ RemoteType::Pbs => todo!(),
+ }
+}
+
+/// Get the changelog for a given package.
+pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Result<String, Error> {
+ match remote.ty {
+ RemoteType::Pve => {
+ let client = connection::make_pve_client(&remote)?;
+
+ client
+ .get_package_changelog(node, package, None)
+ .await
+ .map_err(Into::into)
+ }
+ RemoteType::Pbs => Ok("TODO: Return PBS package changelog".into()),
+ }
+}
+
+async fn fetch_available_updates(
+ remote: Remote,
+ node: String,
+) -> Result<Vec<APTUpdateInfo>, Error> {
+ match remote.ty {
+ RemoteType::Pve => {
+ let client = connection::make_pve_client(&remote)?;
+
+ let updates = client
+ .list_available_updates(&node)
+ .await?
+ .into_iter()
+ .map(map_pve_update_info)
+ .collect();
+
+ Ok(updates)
+ }
+ RemoteType::Pbs => {
+ let _client = connection::make_pbs_client(&remote)?;
+
+ // TODO: Fetch available updates
+ //
+
+ Ok(Vec::new())
+ }
+ }
+}
+
+fn map_pve_update_info(info: pve_api_types::AptUpdateInfo) -> APTUpdateInfo {
+ APTUpdateInfo {
+ package: info.package,
+ title: info.title,
+ arch: info.arch,
+ description: info.description,
+ version: info.version,
+ old_version: info.old_version.unwrap_or_default(),
+ origin: info.origin,
+ priority: info.priority,
+ section: info.section,
+ extra_info: None,
+ }
+}
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 2/4] ui: pve: promote node.rs to dir-style module
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (6 preceding siblings ...)
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab Lukas Wagner
` (3 subsequent siblings)
11 siblings, 0 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
This module will house the different tabs for various views (overview,
updates, etc.).
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
ui/src/pve/{node.rs => node/mod.rs} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename ui/src/pve/{node.rs => node/mod.rs} (100%)
diff --git a/ui/src/pve/node.rs b/ui/src/pve/node/mod.rs
similarity index 100%
rename from ui/src/pve/node.rs
rename to ui/src/pve/node/mod.rs
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (7 preceding siblings ...)
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/4] ui: pve: promote node.rs to dir-style module Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-03 9:10 ` Stefan Hanreich
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/4] ui: pve: node: add update tab Lukas Wagner
` (2 subsequent siblings)
11 siblings, 1 reply; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
This allows us add other tabs later.
No functional changes for the overview component, just moving code
around and adapting as needed to make it work in a tab panel.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
ui/src/pve/node/mod.rs | 320 ++------------------------------
ui/src/pve/node/overview.rs | 358 ++++++++++++++++++++++++++++++++++++
2 files changed, 376 insertions(+), 302 deletions(-)
create mode 100644 ui/src/pve/node/overview.rs
diff --git a/ui/src/pve/node/mod.rs b/ui/src/pve/node/mod.rs
index ff24f7ec..0190611b 100644
--- a/ui/src/pve/node/mod.rs
+++ b/ui/src/pve/node/mod.rs
@@ -5,20 +5,16 @@ use yew::{
Context,
};
-use proxmox_human_byte::HumanByte;
-use proxmox_yew_comp::{RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series};
use pwt::{
- css::{AlignItems, ColorScheme, FlexFit, JustifyContent},
+ css::{AlignItems, ColorScheme},
prelude::*,
props::{ContainerBuilder, WidgetBuilder},
- widget::{error_message, Column, Container, Fa, Panel, Progress, Row},
- AsyncPool,
+ widget::{Fa, Row, TabBarItem, TabPanel},
};
-use pdm_api_types::rrddata::NodeDataPoint;
-use pdm_client::types::NodeStatus;
+mod overview;
-use crate::renderer::separator;
+use overview::NodeOverviewPanel;
#[derive(Clone, Debug, Eq, PartialEq, Properties)]
pub struct NodePanel {
@@ -27,14 +23,6 @@ pub struct NodePanel {
/// The node to show
pub node: String,
-
- #[prop_or(60_000)]
- /// The interval for refreshing the rrd data
- pub rrd_interval: u32,
-
- #[prop_or(10_000)]
- /// The interval for refreshing the status data
- pub status_interval: u32,
}
impl NodePanel {
@@ -49,160 +37,20 @@ impl Into<VNode> for NodePanel {
}
}
-pub enum Msg {
- ReloadRrd,
- ReloadStatus,
- LoadFinished(Result<Vec<NodeDataPoint>, proxmox_client::Error>),
- StatusLoadFinished(Result<NodeStatus, proxmox_client::Error>),
- UpdateRrdTimeframe(RRDTimeframe),
-}
-
-pub struct NodePanelComp {
- time_data: Rc<Vec<i64>>,
- cpu_data: Rc<Series>,
- load_data: Rc<Series>,
- mem_data: Rc<Series>,
- mem_total_data: Rc<Series>,
- status: Option<NodeStatus>,
-
- rrd_time_frame: RRDTimeframe,
-
- last_error: Option<proxmox_client::Error>,
- last_status_error: Option<proxmox_client::Error>,
-
- async_pool: AsyncPool,
- _timeout: Option<gloo_timers::callback::Timeout>,
- _status_timeout: Option<gloo_timers::callback::Timeout>,
-}
-
-impl NodePanelComp {
- async fn reload_rrd(remote: &str, node: &str, rrd_time_frame: RRDTimeframe) -> Msg {
- let res = crate::pdm_client()
- .pve_node_rrddata(remote, node, rrd_time_frame.mode, rrd_time_frame.timeframe)
- .await;
-
- Msg::LoadFinished(res)
- }
-
- async fn reload_status(remote: &str, node: &str) -> Result<NodeStatus, proxmox_client::Error> {
- let status = crate::pdm_client().pve_node_status(remote, node).await?;
- Ok(status)
- }
-}
+pub struct NodePanelComp;
impl yew::Component for NodePanelComp {
- type Message = Msg;
+ type Message = ();
type Properties = NodePanel;
- fn create(ctx: &yew::Context<Self>) -> Self {
- ctx.link().send_message(Msg::ReloadRrd);
- ctx.link().send_message(Msg::ReloadStatus);
- Self {
- time_data: Rc::new(Vec::new()),
- cpu_data: Rc::new(Series::new("", Vec::new())),
- load_data: Rc::new(Series::new("", Vec::new())),
- mem_data: Rc::new(Series::new("", Vec::new())),
- mem_total_data: Rc::new(Series::new("", Vec::new())),
- rrd_time_frame: RRDTimeframe::load(),
- status: None,
- last_error: None,
- last_status_error: None,
- async_pool: AsyncPool::new(),
- _timeout: None,
- _status_timeout: None,
- }
- }
-
- fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
- match msg {
- Msg::ReloadRrd => {
- self._timeout = None;
- let props = ctx.props();
- let remote = props.remote.clone();
- let node = props.node.clone();
- let timeframe = self.rrd_time_frame;
- self.async_pool.send_future(ctx.link().clone(), async move {
- Self::reload_rrd(&remote, &node, timeframe).await
- });
- }
- Msg::ReloadStatus => {
- self._status_timeout = None;
- let props = ctx.props();
- let remote = props.remote.clone();
- let node = props.node.clone();
- self.async_pool.send_future(ctx.link().clone(), async move {
- let res = Self::reload_status(&remote, &node).await;
- Msg::StatusLoadFinished(res)
- });
- }
- Msg::LoadFinished(res) => match res {
- Ok(data_points) => {
- self.last_error = None;
- let mut cpu_vec = Vec::with_capacity(data_points.len());
- let mut load_vec = Vec::with_capacity(data_points.len());
- let mut mem_vec = Vec::with_capacity(data_points.len());
- let mut mem_total_vec = Vec::with_capacity(data_points.len());
- let mut time_vec = Vec::with_capacity(data_points.len());
- for data in data_points {
- cpu_vec.push(data.cpu_current.unwrap_or(f64::NAN));
- load_vec.push(data.cpu_avg1.unwrap_or(f64::NAN));
- mem_vec.push(data.mem_used.unwrap_or(f64::NAN));
- mem_total_vec.push(data.mem_total.unwrap_or(f64::NAN));
- time_vec.push(data.time as i64);
- }
-
- self.cpu_data = Rc::new(Series::new(tr!("CPU"), cpu_vec));
- self.load_data = Rc::new(Series::new(tr!("Server Load"), load_vec));
- self.mem_data = Rc::new(Series::new(tr!("Used Memory"), mem_vec));
- self.mem_total_data = Rc::new(Series::new(tr!("Total Memory"), mem_total_vec));
- self.time_data = Rc::new(time_vec);
-
- let link = ctx.link().clone();
- self._timeout = Some(gloo_timers::callback::Timeout::new(
- ctx.props().rrd_interval,
- move || link.send_message(Msg::ReloadRrd),
- ))
- }
- Err(err) => self.last_error = Some(err),
- },
- Msg::StatusLoadFinished(res) => {
- match res {
- Ok(status) => {
- self.last_status_error = None;
- self.status = Some(status);
- }
- Err(err) => self.last_status_error = Some(err),
- }
- let link = ctx.link().clone();
- self._status_timeout = Some(gloo_timers::callback::Timeout::new(
- ctx.props().status_interval,
- move || link.send_message(Msg::ReloadStatus),
- ))
- }
- Msg::UpdateRrdTimeframe(rrd_time_frame) => {
- self.rrd_time_frame = rrd_time_frame;
- ctx.link().send_message(Msg::ReloadRrd);
- return false;
- }
- }
- true
+ fn create(_ctx: &yew::Context<Self>) -> Self {
+ Self
}
fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
let props = ctx.props();
if props.remote != old_props.remote || props.node != old_props.node {
- self.status = None;
- self.last_status_error = None;
- self.last_error = None;
- self.time_data = Rc::new(Vec::new());
- self.cpu_data = Rc::new(Series::new("", Vec::new()));
- self.load_data = Rc::new(Series::new("", Vec::new()));
- self.mem_data = Rc::new(Series::new("", Vec::new()));
- self.mem_total_data = Rc::new(Series::new("", Vec::new()));
- self.async_pool = AsyncPool::new();
- ctx.link()
- .send_message_batch(vec![Msg::ReloadRrd, Msg::ReloadStatus]);
true
} else {
false
@@ -210,7 +58,8 @@ impl yew::Component for NodePanelComp {
}
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
- let props = ctx.props();
+ let props = ctx.props().clone();
+
let title: Html = Row::new()
.gap(2)
.class(AlignItems::Baseline)
@@ -218,150 +67,17 @@ impl yew::Component for NodePanelComp {
.with_child(tr! {"Node '{0}'", props.node})
.into();
- let mut status_comp = Column::new().gap(2).padding(4);
- let status = self.status.as_ref();
- let cpu = status.map(|s| s.cpu).unwrap_or_default();
- let maxcpu = status.map(|s| s.cpuinfo.cpus).unwrap_or_default();
- let load = status.map(|s| s.loadavg.join(", ")).unwrap_or_default();
-
- let memory = status.map(|s| s.memory.used as u64).unwrap_or_default();
- let maxmem = status.map(|s| s.memory.total as u64).unwrap_or(1);
-
- let root = status.map(|s| s.rootfs.used as u64).unwrap_or_default();
- let maxroot = status.map(|s| s.rootfs.total as u64).unwrap_or(1);
-
- let memory_used = memory as f64 / maxmem as f64;
- let root_used = root as f64 / maxroot as f64;
-
- status_comp = status_comp
- .with_child(make_row(
- tr!("CPU usage"),
- Fa::new("cpu"),
- tr!("{0}% of {1} CPU(s)", format!("{:.2}", cpu * 100.0), maxcpu),
- Some(cpu as f32),
- ))
- .with_child(make_row(
- tr!("Load average"),
- Fa::new("line-chart"),
- load,
- None,
- ))
- .with_child(make_row(
- tr!("Memory usage"),
- Fa::new("memory"),
- tr!(
- "{0}% ({1} of {2})",
- format!("{:.2}", memory_used * 100.0),
- HumanByte::from(memory),
- HumanByte::from(maxmem),
- ),
- Some(memory_used as f32),
- ))
- .with_child(make_row(
- tr!("Root filesystem usage"),
- Fa::new("database"),
- tr!(
- "{0}% ({1} of {2})",
- format!("{:.2}", root_used * 100.0),
- HumanByte::from(root),
- HumanByte::from(maxroot),
- ),
- Some(root_used as f32),
- ))
- .with_child(Container::new().padding(1)) // spacer
- .with_child(
- Row::new()
- .with_child(tr!("Version"))
- .with_flex_spacer()
- .with_optional_child(status.map(|s| s.pveversion.as_str())),
- )
- .with_child(
- Row::new()
- .with_child(tr!("CPU Model"))
- .with_flex_spacer()
- .with_child(tr!(
- "{0} ({1} sockets)",
- status.map(|s| s.cpuinfo.model.as_str()).unwrap_or_default(),
- status.map(|s| s.cpuinfo.sockets).unwrap_or_default()
- )),
- );
-
- if let Some(err) = &self.last_status_error {
- status_comp.add_child(error_message(&err.to_string()));
- }
-
- let loading = self.status.is_none() && self.last_status_error.is_none();
- Panel::new()
- .class(FlexFit)
+ TabPanel::new()
+ .class(pwt::css::FlexFit)
.title(title)
.class(ColorScheme::Neutral)
- .with_child(
- // FIXME: add some 'visible' or 'active' property to the progress
- Progress::new()
- .value((!loading).then_some(0.0))
- .style("opacity", (!loading).then_some("0")),
- )
- .with_child(status_comp)
- .with_child(separator().padding_x(4))
- .with_child(
- Row::new()
- .padding_x(4)
- .padding_y(1)
- .class(JustifyContent::FlexEnd)
- .with_child(
- RRDTimeframeSelector::new()
- .on_change(ctx.link().callback(Msg::UpdateRrdTimeframe)),
- ),
- )
- .with_child(
- Container::new().class(FlexFit).with_child(
- Column::new()
- .padding(4)
- .gap(4)
- .with_child(
- RRDGraph::new(self.time_data.clone())
- .title(tr!("CPU Usage"))
- .render_value(|v: &f64| {
- if v.is_finite() {
- format!("{:.2}%", v * 100.0)
- } else {
- v.to_string()
- }
- })
- .serie0(Some(self.cpu_data.clone())),
- )
- .with_child(
- RRDGraph::new(self.time_data.clone())
- .title(tr!("Server load"))
- .render_value(|v: &f64| {
- if v.is_finite() {
- format!("{:.2}", v)
- } else {
- v.to_string()
- }
- })
- .serie0(Some(self.load_data.clone())),
- )
- .with_child(
- RRDGraph::new(self.time_data.clone())
- .title(tr!("Memory Usage"))
- .binary(true)
- .render_value(|v: &f64| {
- if v.is_finite() {
- proxmox_human_byte::HumanByte::from(*v as u64).to_string()
- } else {
- v.to_string()
- }
- })
- .serie0(Some(self.mem_total_data.clone()))
- .serie1(Some(self.mem_data.clone())),
- ),
- ),
+ .with_item_builder(
+ TabBarItem::new()
+ .key("status_view")
+ .label(tr!("Overview"))
+ .icon_class("fa fa-tachometer"),
+ move |_| NodeOverviewPanel::new(props.remote.clone(), props.node.clone()).into(),
)
.into()
}
}
-
-fn make_row(title: String, icon: Fa, text: String, meter_value: Option<f32>) -> Column {
- crate::renderer::status_row(title, icon, text, meter_value, false)
-}
diff --git a/ui/src/pve/node/overview.rs b/ui/src/pve/node/overview.rs
new file mode 100644
index 00000000..9ebc3e1d
--- /dev/null
+++ b/ui/src/pve/node/overview.rs
@@ -0,0 +1,358 @@
+use std::rc::Rc;
+
+use yew::{
+ virtual_dom::{VComp, VNode},
+ Context,
+};
+
+use proxmox_human_byte::HumanByte;
+use proxmox_yew_comp::{RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series};
+use pwt::{
+ css::{ColorScheme, FlexFit, JustifyContent},
+ prelude::*,
+ props::{ContainerBuilder, WidgetBuilder},
+ widget::{error_message, Column, Container, Fa, Progress, Row},
+ AsyncPool,
+};
+
+use pdm_api_types::rrddata::NodeDataPoint;
+use pdm_client::types::NodeStatus;
+
+use crate::renderer::separator;
+
+#[derive(Clone, Debug, Eq, PartialEq, Properties)]
+pub struct NodeOverviewPanel {
+ /// The remote to show
+ pub remote: String,
+
+ /// The node to show
+ pub node: String,
+
+ #[prop_or(60_000)]
+ /// The interval for refreshing the rrd data
+ pub rrd_interval: u32,
+
+ #[prop_or(10_000)]
+ /// The interval for refreshing the status data
+ pub status_interval: u32,
+}
+
+impl NodeOverviewPanel {
+ pub fn new(remote: String, node: String) -> Self {
+ yew::props!(Self { remote, node })
+ }
+}
+
+impl Into<VNode> for NodeOverviewPanel {
+ fn into(self) -> VNode {
+ VComp::new::<NodeOverviewPanelComp>(Rc::new(self), None).into()
+ }
+}
+
+pub enum Msg {
+ ReloadRrd,
+ ReloadStatus,
+ LoadFinished(Result<Vec<NodeDataPoint>, proxmox_client::Error>),
+ StatusLoadFinished(Result<NodeStatus, proxmox_client::Error>),
+ UpdateRrdTimeframe(RRDTimeframe),
+}
+
+pub struct NodeOverviewPanelComp {
+ time_data: Rc<Vec<i64>>,
+ cpu_data: Rc<Series>,
+ load_data: Rc<Series>,
+ mem_data: Rc<Series>,
+ mem_total_data: Rc<Series>,
+ status: Option<NodeStatus>,
+
+ rrd_time_frame: RRDTimeframe,
+
+ last_error: Option<proxmox_client::Error>,
+ last_status_error: Option<proxmox_client::Error>,
+
+ async_pool: AsyncPool,
+ _timeout: Option<gloo_timers::callback::Timeout>,
+ _status_timeout: Option<gloo_timers::callback::Timeout>,
+}
+
+impl NodeOverviewPanelComp {
+ async fn reload_rrd(remote: &str, node: &str, rrd_time_frame: RRDTimeframe) -> Msg {
+ let res = crate::pdm_client()
+ .pve_node_rrddata(remote, node, rrd_time_frame.mode, rrd_time_frame.timeframe)
+ .await;
+
+ Msg::LoadFinished(res)
+ }
+
+ async fn reload_status(remote: &str, node: &str) -> Result<NodeStatus, proxmox_client::Error> {
+ let status = crate::pdm_client().pve_node_status(remote, node).await?;
+ Ok(status)
+ }
+}
+
+impl yew::Component for NodeOverviewPanelComp {
+ type Message = Msg;
+ type Properties = NodeOverviewPanel;
+
+ fn create(ctx: &yew::Context<Self>) -> Self {
+ ctx.link().send_message(Msg::ReloadRrd);
+ ctx.link().send_message(Msg::ReloadStatus);
+ Self {
+ time_data: Rc::new(Vec::new()),
+ cpu_data: Rc::new(Series::new("", Vec::new())),
+ load_data: Rc::new(Series::new("", Vec::new())),
+ mem_data: Rc::new(Series::new("", Vec::new())),
+ mem_total_data: Rc::new(Series::new("", Vec::new())),
+ rrd_time_frame: RRDTimeframe::load(),
+ status: None,
+ last_error: None,
+ last_status_error: None,
+ async_pool: AsyncPool::new(),
+ _timeout: None,
+ _status_timeout: None,
+ }
+ }
+
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::ReloadRrd => {
+ self._timeout = None;
+ let props = ctx.props();
+ let remote = props.remote.clone();
+ let node = props.node.clone();
+ let timeframe = self.rrd_time_frame;
+ self.async_pool.send_future(ctx.link().clone(), async move {
+ Self::reload_rrd(&remote, &node, timeframe).await
+ });
+ }
+ Msg::ReloadStatus => {
+ self._status_timeout = None;
+ let props = ctx.props();
+ let remote = props.remote.clone();
+ let node = props.node.clone();
+ self.async_pool.send_future(ctx.link().clone(), async move {
+ let res = Self::reload_status(&remote, &node).await;
+ Msg::StatusLoadFinished(res)
+ });
+ }
+ Msg::LoadFinished(res) => match res {
+ Ok(data_points) => {
+ self.last_error = None;
+ let mut cpu_vec = Vec::with_capacity(data_points.len());
+ let mut load_vec = Vec::with_capacity(data_points.len());
+ let mut mem_vec = Vec::with_capacity(data_points.len());
+ let mut mem_total_vec = Vec::with_capacity(data_points.len());
+ let mut time_vec = Vec::with_capacity(data_points.len());
+ for data in data_points {
+ cpu_vec.push(data.cpu_current.unwrap_or(f64::NAN));
+ load_vec.push(data.cpu_avg1.unwrap_or(f64::NAN));
+ mem_vec.push(data.mem_used.unwrap_or(f64::NAN));
+ mem_total_vec.push(data.mem_total.unwrap_or(f64::NAN));
+ time_vec.push(data.time as i64);
+ }
+
+ self.cpu_data = Rc::new(Series::new(tr!("CPU"), cpu_vec));
+ self.load_data = Rc::new(Series::new(tr!("Server Load"), load_vec));
+ self.mem_data = Rc::new(Series::new(tr!("Used Memory"), mem_vec));
+ self.mem_total_data = Rc::new(Series::new(tr!("Total Memory"), mem_total_vec));
+ self.time_data = Rc::new(time_vec);
+
+ let link = ctx.link().clone();
+ self._timeout = Some(gloo_timers::callback::Timeout::new(
+ ctx.props().rrd_interval,
+ move || link.send_message(Msg::ReloadRrd),
+ ))
+ }
+ Err(err) => self.last_error = Some(err),
+ },
+ Msg::StatusLoadFinished(res) => {
+ match res {
+ Ok(status) => {
+ self.last_status_error = None;
+ self.status = Some(status);
+ }
+ Err(err) => self.last_status_error = Some(err),
+ }
+ let link = ctx.link().clone();
+ self._status_timeout = Some(gloo_timers::callback::Timeout::new(
+ ctx.props().status_interval,
+ move || link.send_message(Msg::ReloadStatus),
+ ))
+ }
+ Msg::UpdateRrdTimeframe(rrd_time_frame) => {
+ self.rrd_time_frame = rrd_time_frame;
+ ctx.link().send_message(Msg::ReloadRrd);
+ return false;
+ }
+ }
+ true
+ }
+
+ fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
+ let props = ctx.props();
+
+ if props.remote != old_props.remote || props.node != old_props.node {
+ self.status = None;
+ self.last_status_error = None;
+ self.last_error = None;
+ self.time_data = Rc::new(Vec::new());
+ self.cpu_data = Rc::new(Series::new("", Vec::new()));
+ self.load_data = Rc::new(Series::new("", Vec::new()));
+ self.mem_data = Rc::new(Series::new("", Vec::new()));
+ self.mem_total_data = Rc::new(Series::new("", Vec::new()));
+ self.async_pool = AsyncPool::new();
+ ctx.link()
+ .send_message_batch(vec![Msg::ReloadRrd, Msg::ReloadStatus]);
+ true
+ } else {
+ false
+ }
+ }
+
+ fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
+ let mut status_comp = Column::new().gap(2).padding(4);
+ let status = self.status.as_ref();
+ let cpu = status.map(|s| s.cpu).unwrap_or_default();
+ let maxcpu = status.map(|s| s.cpuinfo.cpus).unwrap_or_default();
+ let load = status.map(|s| s.loadavg.join(", ")).unwrap_or_default();
+
+ let memory = status.map(|s| s.memory.used as u64).unwrap_or_default();
+ let maxmem = status.map(|s| s.memory.total as u64).unwrap_or(1);
+
+ let root = status.map(|s| s.rootfs.used as u64).unwrap_or_default();
+ let maxroot = status.map(|s| s.rootfs.total as u64).unwrap_or(1);
+
+ let memory_used = memory as f64 / maxmem as f64;
+ let root_used = root as f64 / maxroot as f64;
+
+ status_comp = status_comp
+ .with_child(make_row(
+ tr!("CPU usage"),
+ Fa::new("cpu"),
+ tr!("{0}% of {1} CPU(s)", format!("{:.2}", cpu * 100.0), maxcpu),
+ Some(cpu as f32),
+ ))
+ .with_child(make_row(
+ tr!("Load average"),
+ Fa::new("line-chart"),
+ load,
+ None,
+ ))
+ .with_child(make_row(
+ tr!("Memory usage"),
+ Fa::new("memory"),
+ tr!(
+ "{0}% ({1} of {2})",
+ format!("{:.2}", memory_used * 100.0),
+ HumanByte::from(memory),
+ HumanByte::from(maxmem),
+ ),
+ Some(memory_used as f32),
+ ))
+ .with_child(make_row(
+ tr!("Root filesystem usage"),
+ Fa::new("database"),
+ tr!(
+ "{0}% ({1} of {2})",
+ format!("{:.2}", root_used * 100.0),
+ HumanByte::from(root),
+ HumanByte::from(maxroot),
+ ),
+ Some(root_used as f32),
+ ))
+ .with_child(Container::new().padding(1)) // spacer
+ .with_child(
+ Row::new()
+ .with_child(tr!("Version"))
+ .with_flex_spacer()
+ .with_optional_child(status.map(|s| s.pveversion.as_str())),
+ )
+ .with_child(
+ Row::new()
+ .with_child(tr!("CPU Model"))
+ .with_flex_spacer()
+ .with_child(tr!(
+ "{0} ({1} sockets)",
+ status.map(|s| s.cpuinfo.model.as_str()).unwrap_or_default(),
+ status.map(|s| s.cpuinfo.sockets).unwrap_or_default()
+ )),
+ );
+
+ if let Some(err) = &self.last_status_error {
+ status_comp.add_child(error_message(&err.to_string()));
+ }
+
+ let loading = self.status.is_none() && self.last_status_error.is_none();
+ Container::new()
+ .class(FlexFit)
+ .class(ColorScheme::Neutral)
+ .with_child(
+ // FIXME: add some 'visible' or 'active' property to the progress
+ Progress::new()
+ .value((!loading).then_some(0.0))
+ .style("opacity", (!loading).then_some("0")),
+ )
+ .with_child(status_comp)
+ .with_child(separator().padding_x(4))
+ .with_child(
+ Row::new()
+ .padding_x(4)
+ .padding_y(1)
+ .class(JustifyContent::FlexEnd)
+ .with_child(
+ RRDTimeframeSelector::new()
+ .on_change(ctx.link().callback(Msg::UpdateRrdTimeframe)),
+ ),
+ )
+ .with_child(
+ Container::new().class(FlexFit).with_child(
+ Column::new()
+ .padding(4)
+ .gap(4)
+ .with_child(
+ RRDGraph::new(self.time_data.clone())
+ .title(tr!("CPU Usage"))
+ .render_value(|v: &f64| {
+ if v.is_finite() {
+ format!("{:.2}%", v * 100.0)
+ } else {
+ v.to_string()
+ }
+ })
+ .serie0(Some(self.cpu_data.clone())),
+ )
+ .with_child(
+ RRDGraph::new(self.time_data.clone())
+ .title(tr!("Server load"))
+ .render_value(|v: &f64| {
+ if v.is_finite() {
+ format!("{:.2}", v)
+ } else {
+ v.to_string()
+ }
+ })
+ .serie0(Some(self.load_data.clone())),
+ )
+ .with_child(
+ RRDGraph::new(self.time_data.clone())
+ .title(tr!("Memory Usage"))
+ .binary(true)
+ .render_value(|v: &f64| {
+ if v.is_finite() {
+ proxmox_human_byte::HumanByte::from(*v as u64).to_string()
+ } else {
+ v.to_string()
+ }
+ })
+ .serie0(Some(self.mem_total_data.clone()))
+ .serie1(Some(self.mem_data.clone())),
+ ),
+ ),
+ )
+ .into()
+ }
+}
+
+fn make_row(title: String, icon: Fa, text: String, meter_value: Option<f32>) -> Column {
+ crate::renderer::status_row(title, icon, text, meter_value, false)
+}
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] [PATCH proxmox-datacenter-manager 4/4] ui: pve: node: add update tab
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (8 preceding siblings ...)
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab Lukas Wagner
@ 2025-09-02 15:14 ` Lukas Wagner
2025-09-03 10:20 ` [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Stefan Hanreich
2025-09-03 11:43 ` [pdm-devel] superseded: " Lukas Wagner
11 siblings, 0 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-02 15:14 UTC (permalink / raw)
To: pdm-devel
This adds a new tab for PVE nodes where available updates are listed.
The 'Upgrade' button is disabled for now, this needs additional work.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
ui/src/pve/node/mod.rs | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/ui/src/pve/node/mod.rs b/ui/src/pve/node/mod.rs
index 0190611b..2ef2aaea 100644
--- a/ui/src/pve/node/mod.rs
+++ b/ui/src/pve/node/mod.rs
@@ -1,5 +1,6 @@
use std::rc::Rc;
+use proxmox_yew_comp::AptPackageManager;
use yew::{
virtual_dom::{VComp, VNode},
Context,
@@ -67,6 +68,9 @@ impl yew::Component for NodePanelComp {
.with_child(tr! {"Node '{0}'", props.node})
.into();
+ let remote = props.remote.clone();
+ let node = props.node.clone();
+
TabPanel::new()
.class(pwt::css::FlexFit)
.title(title)
@@ -78,6 +82,22 @@ impl yew::Component for NodePanelComp {
.icon_class("fa fa-tachometer"),
move |_| NodeOverviewPanel::new(props.remote.clone(), props.node.clone()).into(),
)
+ .with_item_builder(
+ TabBarItem::new()
+ .key("update_view")
+ .label(tr!("Updates"))
+ .icon_class("fa fa-refresh"),
+ move |_| {
+ let base_url = format!("/pve/remotes/{remote}/nodes/{node}/apt");
+ let task_base_url = format!("/pve/remotes/{remote}/tasks");
+
+ AptPackageManager::new()
+ .base_url(base_url)
+ .task_base_url(task_base_url)
+ .enable_upgrade(false)
+ .into()
+ },
+ )
.into()
}
}
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] applied: [PATCH manager 1/1] api: apt: add JSON schema for 'list_updates' endpoint
2025-09-02 15:14 ` [pdm-devel] [PATCH manager 1/1] api: apt: add JSON schema for 'list_updates' endpoint Lukas Wagner
@ 2025-09-03 8:25 ` Thomas Lamprecht
0 siblings, 0 replies; 25+ messages in thread
From: Thomas Lamprecht @ 2025-09-03 8:25 UTC (permalink / raw)
To: pve-devel, pdm-devel, Lukas Wagner
On Tue, 02 Sep 2025 17:14:18 +0200, Lukas Wagner wrote:
> This will be shown in the API viewer. Additionally, this allows us to
> automatically generate the appropriate Rust API type automatically.
>
>
Applied, thanks!
[1/1] api: apt: add JSON schema for 'list_updates' endpoint
commit: be712f0e6a2723ed9b42f74ca7b8aa631a832ee3
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes Lukas Wagner
@ 2025-09-03 8:42 ` Lukas Wagner
2025-09-03 9:02 ` Stefan Hanreich
1 sibling, 0 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-03 8:42 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion; +Cc: pdm-devel
On Tue Sep 2, 2025 at 5:14 PM CEST, Lukas Wagner wrote:
> This adds new APIs for update management:
>
> GET /pve/remotes/{remote}/nodes/{node}/apt/changelog
> -> get package changelog
> GET /pve/remotes/{remote}/nodes/{node}/apt/update
> -> get list of updatable packages
> POST /pve/remotes/{remote}/nodes/{node}/apt/update
> -> refresh APT database
>
> At this time these just pass the call through to PVE with no caching
> involved on the PDM side. This should be fine for this API, but once
> we have an API for 'give me a view of ALL available remote updates',
> we need to introduce a cache that is periodically refreshed.
>
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
> server/src/api/pve/apt.rs | 119 +++++++++++++++++++++++++++++++++++
> server/src/api/pve/mod.rs | 3 +-
> server/src/api/pve/node.rs | 1 +
> server/src/lib.rs | 1 +
> server/src/remote_updates.rs | 96 ++++++++++++++++++++++++++++
> 5 files changed, 219 insertions(+), 1 deletion(-)
> create mode 100644 server/src/api/pve/apt.rs
> create mode 100644 server/src/remote_updates.rs
>
> diff --git a/server/src/api/pve/apt.rs b/server/src/api/pve/apt.rs
> new file mode 100644
> index 00000000..f5027fb8
> --- /dev/null
> +++ b/server/src/api/pve/apt.rs
> @@ -0,0 +1,119 @@
> +use anyhow::Error;
> +
> +use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
> +use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
> +use proxmox_schema::api;
> +use proxmox_schema::api_types::NODE_SCHEMA;
> +
> +use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, RemoteUpid, PRIV_RESOURCE_MODIFY};
> +
> +use crate::{api::remotes::get_remote, remote_updates};
> +
> +#[api(
> + input: {
> + properties: {
> + remote: {
> + schema: REMOTE_ID_SCHEMA,
> + },
> + node: {
> + schema: NODE_SCHEMA,
> + },
> + },
> + },
> + returns: {
> + description: "A list of packages with available updates.",
> + type: Array,
> + items: {
> + type: APTUpdateInfo
> + },
> + },
> + access: {
> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
> + },
> +)]
> +/// List available APT updates for a remote PVE node.
> +async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
> + let (config, _digest) = pdm_config::remotes::config()?;
> + let remote = get_remote(&config, &remote)?;
> +
> + let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
> +
> + Ok(updates)
> +}
> +
> +#[api(
> + input: {
> + properties: {
> + remote: {
> + schema: REMOTE_ID_SCHEMA,
> + },
> + node: {
> + schema: NODE_SCHEMA,
> + },
> + },
> + },
> + access: {
> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
> + },
> +)]
> +/// Update the APT database of a remote PVE node.
> +pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
> + let (config, _digest) = pdm_config::remotes::config()?;
> + let remote = get_remote(&config, &remote)?;
> +
> + let upid = remote_updates::update_apt_database(remote, &node).await?;
> +
> + Ok(upid)
> +}
> +
> +#[api(
> + input: {
> + properties: {
> + remote: {
> + schema: REMOTE_ID_SCHEMA,
> + },
> + node: {
> + schema: NODE_SCHEMA,
> + },
> + options: {
> + type: APTGetChangelogOptions,
> + flatten: true,
> + },
> + },
> + },
> + returns: {
> + description: "The Package changelog.",
> + type: String,
> + },
> + access: {
> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
> + },
> +)]
> +/// Retrieve the changelog of the specified package for a remote PVE node.
> +async fn apt_get_changelog(
> + remote: String,
> + node: String,
> + options: APTGetChangelogOptions,
> +) -> Result<String, Error> {
> + let (config, _digest) = pdm_config::remotes::config()?;
> + let remote = get_remote(&config, &remote)?;
> +
> + remote_updates::get_changelog(remote.clone(), &node, options.name).await
> +}
> +
> +const SUBDIRS: SubdirMap = &[
> + (
> + "changelog",
> + &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
> + ),
> + (
> + "update",
> + &Router::new()
> + .get(&API_METHOD_APT_UPDATE_AVAILABLE)
> + .post(&API_METHOD_APT_UPDATE_DATABASE),
> + ),
> +];
> +
> +pub const ROUTER: Router = Router::new()
> + .get(&list_subdirs_api_method!(SUBDIRS))
> + .subdirs(SUBDIRS);
> diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
> index 2cfdc5b7..0768083d 100644
> --- a/server/src/api/pve/mod.rs
> +++ b/server/src/api/pve/mod.rs
> @@ -31,6 +31,7 @@ use crate::connection::PveClient;
> use crate::connection::{self, probe_tls_connection};
> use crate::remote_tasks;
>
> +mod apt;
> mod lxc;
> mod node;
> mod qemu;
> @@ -77,7 +78,7 @@ const RESOURCES_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_RESOURCES
> const STATUS_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_STATUS);
>
> // converts a remote + PveUpid into a RemoteUpid and starts tracking it
> -async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
> +pub async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
> let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
> remote_tasks::track_running_task(remote_upid.clone()).await?;
> Ok(remote_upid)
> diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
> index df96a1c3..99539d1c 100644
> --- a/server/src/api/pve/node.rs
> +++ b/server/src/api/pve/node.rs
> @@ -13,6 +13,7 @@ pub const ROUTER: Router = Router::new()
>
> #[sortable]
> const SUBDIRS: SubdirMap = &sorted!([
> + ("apt", &super::apt::ROUTER),
> ("rrddata", &super::rrddata::NODE_RRD_ROUTER),
> ("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
> ("storage", &Router::new().get(&API_METHOD_GET_STORAGES)),
> diff --git a/server/src/lib.rs b/server/src/lib.rs
> index 3f8b7708..a58190d8 100644
> --- a/server/src/lib.rs
> +++ b/server/src/lib.rs
> @@ -9,6 +9,7 @@ pub mod metric_collection;
> pub mod parallel_fetcher;
> pub mod remote_cache;
> pub mod remote_tasks;
> +pub mod remote_updates;
> pub mod resource_cache;
> pub mod task_utils;
>
> diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
> new file mode 100644
> index 00000000..809e5de1
> --- /dev/null
> +++ b/server/src/remote_updates.rs
> @@ -0,0 +1,96 @@
> +use anyhow::Error;
> +use pdm_api_types::RemoteUpid;
> +
> +use proxmox_apt_api_types::APTUpdateInfo;
> +
> +use pdm_api_types::remotes::{Remote, RemoteType};
> +
> +use crate::api::pve::new_remote_upid;
> +use crate::connection;
> +
> +/// Return a list of available updates for a given remote node.
> +pub async fn list_available_updates(
> + remote: Remote,
> + node: &str,
> +) -> Result<Vec<APTUpdateInfo>, Error> {
> + let updates = fetch_available_updates(remote, node.to_string()).await?;
> + Ok(updates)
> +}
> +
> +/// Trigger `apt update` on a remote node.
> +///
> +/// The function returns a `[RemoteUpid]` for the started update task.
> +pub async fn update_apt_database(remote: &Remote, node: &str) -> Result<RemoteUpid, Error> {
> + match remote.ty {
> + RemoteType::Pve => {
> + let client = connection::make_pve_client(remote)?;
> +
> + let params = pve_api_types::AptUpdateParams {
> + notify: Some(false),
> + quiet: Some(false),
> + };
> + let upid = client.update_apt_database(node, params).await?;
> +
> + new_remote_upid(remote.id.clone(), upid).await
> + }
> + RemoteType::Pbs => todo!(),
Missed that one during my self review yesterday, I'll replace this with
a `bail!` in v2, which I will post once I get first review feedback from
somebody else.
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] applied: [PATCH proxmox-api-types 1/3] Schema2Rust: fix handling of non-optional params
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 1/3] Schema2Rust: fix handling of non-optional params Lukas Wagner
@ 2025-09-03 8:57 ` Wolfgang Bumiller
0 siblings, 0 replies; 25+ messages in thread
From: Wolfgang Bumiller @ 2025-09-03 8:57 UTC (permalink / raw)
To: Lukas Wagner; +Cc: pdm-devel
applied, thanks
On Tue, Sep 02, 2025 at 05:14:19PM +0200, Lukas Wagner wrote:
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
> pve-api-types/generator-lib/Schema2Rust.pm | 9 +++++++--
> 1 file changed, 7 insertions(+), 2 deletions(-)
>
> diff --git a/pve-api-types/generator-lib/Schema2Rust.pm b/pve-api-types/generator-lib/Schema2Rust.pm
> index 77cb373..436ba76 100644
> --- a/pve-api-types/generator-lib/Schema2Rust.pm
> +++ b/pve-api-types/generator-lib/Schema2Rust.pm
> @@ -497,8 +497,10 @@ my sub print_method_without_body : prototype($$$$$) {
> print {$out} " .maybe_bool_arg(\"$name\", p_$rust_name)\n";
> } elsif ($arg->{is_string_list}) {
> print {$out} " .maybe_list_arg(\"$name\", p_$rust_name)\n";
> - } else {
> + } elsif ($arg->{optional}) {
> print {$out} " .maybe_arg(\"$name\", &p_$rust_name)\n";
> + } else {
> + print {$out} " .arg(\"$name\", &p_$rust_name)\n";
> }
> }
> print {$out} " .build();\n";
> @@ -517,12 +519,15 @@ my sub print_method_without_body : prototype($$$$$) {
> for my $arg (@$input) {
> my $name = $arg->{name};
> my $rust_name = $arg->{rust_name};
> +
> if ($arg->{type} eq 'Option<bool>') {
> print {$out} " .maybe_bool_arg(\"$name\", $rust_name)\n";
> } elsif ($arg->{is_string_list}) {
> print {$out} " .maybe_list_arg(\"$name\", &$rust_name)\n";
> - } else {
> + } elsif ($arg->{optional}) {
> print {$out} " .maybe_arg(\"$name\", &$rust_name)\n";
> + } else {
> + print {$out} " .arg(\"$name\", &$rust_name)\n";
> }
> }
> print {$out} " .build();\n";
> --
> 2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] applied: [PATCH proxmox-api-types 2/3] generate: add bindings for various APT functions
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 2/3] generate: add bindings for various APT functions Lukas Wagner
@ 2025-09-03 8:58 ` Wolfgang Bumiller
0 siblings, 0 replies; 25+ messages in thread
From: Wolfgang Bumiller @ 2025-09-03 8:58 UTC (permalink / raw)
To: Lukas Wagner; +Cc: pdm-devel
applied, thanks
On Tue, Sep 02, 2025 at 05:14:20PM +0200, Lukas Wagner wrote:
> This adds bindings for
> GET /nodes/<node>/apt/update
> POST /nodes/<node>/apt/update
> GET /nodes/<node>/apt/changelog
>
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
> pve-api-types/generate.pl | 4 ++++
> 1 file changed, 4 insertions(+)
>
> diff --git a/pve-api-types/generate.pl b/pve-api-types/generate.pl
> index 966c4d0..c3bece0 100644
> --- a/pve-api-types/generate.pl
> +++ b/pve-api-types/generate.pl
> @@ -330,6 +330,10 @@ Schema2Rust::derive('ListRealm' => 'Clone', 'PartialEq');
> api(POST => '/access/users/{userid}/token/{tokenid}', 'create_token', 'param-name' => 'CreateToken');
> Schema2Rust::derive('CreateToken' => 'Default');
>
> +api(GET => '/nodes/{node}/apt/update', 'list_available_updates', 'return-name' => 'AptUpdateInfo');
> +api(POST => '/nodes/{node}/apt/update', 'update_apt_database', 'output-type' => 'PveUpid', 'param-name' => 'AptUpdateParams');
> +api(GET => '/nodes/{node}/apt/changelog', 'get_package_changelog', 'output-type' => 'String');
> +
> # NOW DUMP THE CODE:
> #
> # We generate one file for API types, and one for API method calls.
> --
> 2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-api-types 3/3] refresh bindings
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 3/3] refresh bindings Lukas Wagner
@ 2025-09-03 8:59 ` Wolfgang Bumiller
0 siblings, 0 replies; 25+ messages in thread
From: Wolfgang Bumiller @ 2025-09-03 8:59 UTC (permalink / raw)
To: Lukas Wagner; +Cc: pdm-devel
Since I pushed a change to use a JSON file as API input I replaced this
with the pve-api.json refresh followed by the regeneration.
Also it looks like this was diverged from the current master state (the
doc comment updates were missing?)
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes Lukas Wagner
2025-09-03 8:42 ` Lukas Wagner
@ 2025-09-03 9:02 ` Stefan Hanreich
2025-09-03 9:19 ` Stefan Hanreich
2025-09-03 9:23 ` Lukas Wagner
1 sibling, 2 replies; 25+ messages in thread
From: Stefan Hanreich @ 2025-09-03 9:02 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Lukas Wagner
On 9/2/25 5:14 PM, Lukas Wagner wrote:
> This adds new APIs for update management:
>
> GET /pve/remotes/{remote}/nodes/{node}/apt/changelog
> -> get package changelog
> GET /pve/remotes/{remote}/nodes/{node}/apt/update
> -> get list of updatable packages
> POST /pve/remotes/{remote}/nodes/{node}/apt/update
> -> refresh APT database
>
> At this time these just pass the call through to PVE with no caching
> involved on the PDM side. This should be fine for this API, but once
> we have an API for 'give me a view of ALL available remote updates',
> we need to introduce a cache that is periodically refreshed.
>
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
> server/src/api/pve/apt.rs | 119 +++++++++++++++++++++++++++++++++++
> server/src/api/pve/mod.rs | 3 +-
> server/src/api/pve/node.rs | 1 +
> server/src/lib.rs | 1 +
> server/src/remote_updates.rs | 96 ++++++++++++++++++++++++++++
> 5 files changed, 219 insertions(+), 1 deletion(-)
> create mode 100644 server/src/api/pve/apt.rs
> create mode 100644 server/src/remote_updates.rs
>
> diff --git a/server/src/api/pve/apt.rs b/server/src/api/pve/apt.rs
> new file mode 100644
> index 00000000..f5027fb8
> --- /dev/null
> +++ b/server/src/api/pve/apt.rs
> @@ -0,0 +1,119 @@
> +use anyhow::Error;
> +
> +use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
> +use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
> +use proxmox_schema::api;
> +use proxmox_schema::api_types::NODE_SCHEMA;
> +
> +use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, RemoteUpid, PRIV_RESOURCE_MODIFY};
> +
> +use crate::{api::remotes::get_remote, remote_updates};
> +
> +#[api(
> + input: {
> + properties: {
> + remote: {
> + schema: REMOTE_ID_SCHEMA,
> + },
> + node: {
> + schema: NODE_SCHEMA,
> + },
> + },
> + },
> + returns: {
> + description: "A list of packages with available updates.",
> + type: Array,
> + items: {
> + type: APTUpdateInfo
> + },
> + },
> + access: {
> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
> + },
> +)]
> +/// List available APT updates for a remote PVE node.
> +async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
potentially fine to take a ref here for both params as well?
> + let (config, _digest) = pdm_config::remotes::config()?;
> + let remote = get_remote(&config, &remote)?;
> +
> + let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
> +
> + Ok(updates)
> +}
> +
> +#[api(
> + input: {
> + properties: {
> + remote: {
> + schema: REMOTE_ID_SCHEMA,
> + },
> + node: {
> + schema: NODE_SCHEMA,
> + },
> + },
> + },
> + access: {
> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
> + },
> +)]
> +/// Update the APT database of a remote PVE node.
> +pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
here too then
> + let (config, _digest) = pdm_config::remotes::config()?;
> + let remote = get_remote(&config, &remote)?;
> +
> + let upid = remote_updates::update_apt_database(remote, &node).await?;
> +
> + Ok(upid)
> +}
> +
> +#[api(
> + input: {
> + properties: {
> + remote: {
> + schema: REMOTE_ID_SCHEMA,
> + },
> + node: {
> + schema: NODE_SCHEMA,
> + },
> + options: {
> + type: APTGetChangelogOptions,
> + flatten: true,
> + },
> + },
> + },
> + returns: {
> + description: "The Package changelog.",
> + type: String,
> + },
> + access: {
> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
> + },
> +)]
> +/// Retrieve the changelog of the specified package for a remote PVE node.
> +async fn apt_get_changelog(
> + remote: String,
> + node: String,
> + options: APTGetChangelogOptions,
here too then
> +) -> Result<String, Error> {
> + let (config, _digest) = pdm_config::remotes::config()?;
> + let remote = get_remote(&config, &remote)?;
> +
> + remote_updates::get_changelog(remote.clone(), &node, options.name).await
> +}
> +
> +const SUBDIRS: SubdirMap = &[
> + (
> + "changelog",
> + &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
> + ),
> + (
> + "update",
> + &Router::new()
> + .get(&API_METHOD_APT_UPDATE_AVAILABLE)
> + .post(&API_METHOD_APT_UPDATE_DATABASE),
> + ),
> +];
> +
> +pub const ROUTER: Router = Router::new()
> + .get(&list_subdirs_api_method!(SUBDIRS))
> + .subdirs(SUBDIRS);
> diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
> index 2cfdc5b7..0768083d 100644
> --- a/server/src/api/pve/mod.rs
> +++ b/server/src/api/pve/mod.rs
> @@ -31,6 +31,7 @@ use crate::connection::PveClient;
> use crate::connection::{self, probe_tls_connection};
> use crate::remote_tasks;
>
> +mod apt;
> mod lxc;
> mod node;
> mod qemu;
> @@ -77,7 +78,7 @@ const RESOURCES_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_RESOURCES
> const STATUS_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_STATUS);
>
> // converts a remote + PveUpid into a RemoteUpid and starts tracking it
> -async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
> +pub async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
> let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
> remote_tasks::track_running_task(remote_upid.clone()).await?;
> Ok(remote_upid)
> diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
> index df96a1c3..99539d1c 100644
> --- a/server/src/api/pve/node.rs
> +++ b/server/src/api/pve/node.rs
> @@ -13,6 +13,7 @@ pub const ROUTER: Router = Router::new()
>
> #[sortable]
> const SUBDIRS: SubdirMap = &sorted!([
> + ("apt", &super::apt::ROUTER),
> ("rrddata", &super::rrddata::NODE_RRD_ROUTER),
> ("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
> ("storage", &Router::new().get(&API_METHOD_GET_STORAGES)),
> diff --git a/server/src/lib.rs b/server/src/lib.rs
> index 3f8b7708..a58190d8 100644
> --- a/server/src/lib.rs
> +++ b/server/src/lib.rs
> @@ -9,6 +9,7 @@ pub mod metric_collection;
> pub mod parallel_fetcher;
> pub mod remote_cache;
> pub mod remote_tasks;
> +pub mod remote_updates;
> pub mod resource_cache;
> pub mod task_utils;
>
> diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
> new file mode 100644
> index 00000000..809e5de1
> --- /dev/null
> +++ b/server/src/remote_updates.rs
> @@ -0,0 +1,96 @@
> +use anyhow::Error;
> +use pdm_api_types::RemoteUpid;
> +
> +use proxmox_apt_api_types::APTUpdateInfo;
> +
> +use pdm_api_types::remotes::{Remote, RemoteType};
> +
> +use crate::api::pve::new_remote_upid;
> +use crate::connection;
> +
> +/// Return a list of available updates for a given remote node.
> +pub async fn list_available_updates(
> + remote: Remote,
> + node: &str,
> +) -> Result<Vec<APTUpdateInfo>, Error> {
> + let updates = fetch_available_updates(remote, node.to_string()).await?;
> + Ok(updates)
> +}
> +
Is there a reason for having this as an additional function and not just
calling fetch_available_updates at its callsites (after changing its
visibility)?
At the very least it could return the result of the await directly? Or
do you want to reserve the option of changing the public API? In that
case we could still introduce a private helper later.
> +/// Trigger `apt update` on a remote node.
> +///
> +/// The function returns a `[RemoteUpid]` for the started update task.
> +pub async fn update_apt_database(remote: &Remote, node: &str) -> Result<RemoteUpid, Error> {
> + match remote.ty {
> + RemoteType::Pve => {
> + let client = connection::make_pve_client(remote)?;
> +
> + let params = pve_api_types::AptUpdateParams {
> + notify: Some(false),
> + quiet: Some(false),
> + };
> + let upid = client.update_apt_database(node, params).await?;
> +
> + new_remote_upid(remote.id.clone(), upid).await
> + }
> + RemoteType::Pbs => todo!(),
return an Error here?
> + }
> +}
> +
> +/// Get the changelog for a given package.
> +pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Result<String, Error> {
Interestingly the generated binding takes a String even though it
wouldn't need to? Maybe something we should check in the generation
logic later?
> + match remote.ty {
> + RemoteType::Pve => {
> + let client = connection::make_pve_client(&remote)?;
> +
> + client
> + .get_package_changelog(node, package, None)
> + .await
> + .map_err(Into::into)
> + }
> + RemoteType::Pbs => Ok("TODO: Return PBS package changelog".into()),
> + }
> +}
> +
> +async fn fetch_available_updates(
> + remote: Remote,
> + node: String,
take a ref here too?
> +) -> Result<Vec<APTUpdateInfo>, Error> {
> + match remote.ty {
> + RemoteType::Pve => {
> + let client = connection::make_pve_client(&remote)?;
> +
> + let updates = client
> + .list_available_updates(&node)
> + .await?
> + .into_iter()
> + .map(map_pve_update_info)
> + .collect();
> +
> + Ok(updates)
> + }
> + RemoteType::Pbs => {
> + let _client = connection::make_pbs_client(&remote)?;
> +
> + // TODO: Fetch available updates
> + //
> +
> + Ok(Vec::new())
> + }
> + }
> +}
> +
> +fn map_pve_update_info(info: pve_api_types::AptUpdateInfo) -> APTUpdateInfo {
> + APTUpdateInfo {
> + package: info.package,
> + title: info.title,
> + arch: info.arch,
> + description: info.description,
> + version: info.version,
> + old_version: info.old_version.unwrap_or_default(),
> + origin: info.origin,
> + priority: info.priority,
> + section: info.section,
> + extra_info: None,
> + }
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab Lukas Wagner
@ 2025-09-03 9:10 ` Stefan Hanreich
2025-09-03 9:48 ` Lukas Wagner
0 siblings, 1 reply; 25+ messages in thread
From: Stefan Hanreich @ 2025-09-03 9:10 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Lukas Wagner
one small nit inline (pre-existing) I noticed while looking over the changes
On 9/2/25 5:14 PM, Lukas Wagner wrote:
> This allows us add other tabs later.
>
> No functional changes for the overview component, just moving code
> around and adapting as needed to make it work in a tab panel.
>
> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
> ---
> ui/src/pve/node/mod.rs | 320 ++------------------------------
> ui/src/pve/node/overview.rs | 358 ++++++++++++++++++++++++++++++++++++
> 2 files changed, 376 insertions(+), 302 deletions(-)
> create mode 100644 ui/src/pve/node/overview.rs
>
> diff --git a/ui/src/pve/node/mod.rs b/ui/src/pve/node/mod.rs
> index ff24f7ec..0190611b 100644
> --- a/ui/src/pve/node/mod.rs
> +++ b/ui/src/pve/node/mod.rs
> @@ -5,20 +5,16 @@ use yew::{
> Context,
> };
>
> -use proxmox_human_byte::HumanByte;
> -use proxmox_yew_comp::{RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series};
> use pwt::{
> - css::{AlignItems, ColorScheme, FlexFit, JustifyContent},
> + css::{AlignItems, ColorScheme},
> prelude::*,
> props::{ContainerBuilder, WidgetBuilder},
> - widget::{error_message, Column, Container, Fa, Panel, Progress, Row},
> - AsyncPool,
> + widget::{Fa, Row, TabBarItem, TabPanel},
> };
>
> -use pdm_api_types::rrddata::NodeDataPoint;
> -use pdm_client::types::NodeStatus;
> +mod overview;
>
> -use crate::renderer::separator;
> +use overview::NodeOverviewPanel;
>
> #[derive(Clone, Debug, Eq, PartialEq, Properties)]
> pub struct NodePanel {
> @@ -27,14 +23,6 @@ pub struct NodePanel {
>
> /// The node to show
> pub node: String,
> -
> - #[prop_or(60_000)]
> - /// The interval for refreshing the rrd data
> - pub rrd_interval: u32,
> -
> - #[prop_or(10_000)]
> - /// The interval for refreshing the status data
> - pub status_interval: u32,
> }
>
> impl NodePanel {
> @@ -49,160 +37,20 @@ impl Into<VNode> for NodePanel {
> }
> }
>
> -pub enum Msg {
> - ReloadRrd,
> - ReloadStatus,
> - LoadFinished(Result<Vec<NodeDataPoint>, proxmox_client::Error>),
> - StatusLoadFinished(Result<NodeStatus, proxmox_client::Error>),
> - UpdateRrdTimeframe(RRDTimeframe),
> -}
> -
> -pub struct NodePanelComp {
> - time_data: Rc<Vec<i64>>,
> - cpu_data: Rc<Series>,
> - load_data: Rc<Series>,
> - mem_data: Rc<Series>,
> - mem_total_data: Rc<Series>,
> - status: Option<NodeStatus>,
> -
> - rrd_time_frame: RRDTimeframe,
> -
> - last_error: Option<proxmox_client::Error>,
> - last_status_error: Option<proxmox_client::Error>,
> -
> - async_pool: AsyncPool,
> - _timeout: Option<gloo_timers::callback::Timeout>,
> - _status_timeout: Option<gloo_timers::callback::Timeout>,
> -}
> -
> -impl NodePanelComp {
> - async fn reload_rrd(remote: &str, node: &str, rrd_time_frame: RRDTimeframe) -> Msg {
> - let res = crate::pdm_client()
> - .pve_node_rrddata(remote, node, rrd_time_frame.mode, rrd_time_frame.timeframe)
> - .await;
> -
> - Msg::LoadFinished(res)
> - }
> -
> - async fn reload_status(remote: &str, node: &str) -> Result<NodeStatus, proxmox_client::Error> {
> - let status = crate::pdm_client().pve_node_status(remote, node).await?;
> - Ok(status)
> - }
> -}
> +pub struct NodePanelComp;
>
> impl yew::Component for NodePanelComp {
> - type Message = Msg;
> + type Message = ();
> type Properties = NodePanel;
>
> - fn create(ctx: &yew::Context<Self>) -> Self {
> - ctx.link().send_message(Msg::ReloadRrd);
> - ctx.link().send_message(Msg::ReloadStatus);
> - Self {
> - time_data: Rc::new(Vec::new()),
> - cpu_data: Rc::new(Series::new("", Vec::new())),
> - load_data: Rc::new(Series::new("", Vec::new())),
> - mem_data: Rc::new(Series::new("", Vec::new())),
> - mem_total_data: Rc::new(Series::new("", Vec::new())),
> - rrd_time_frame: RRDTimeframe::load(),
> - status: None,
> - last_error: None,
> - last_status_error: None,
> - async_pool: AsyncPool::new(),
> - _timeout: None,
> - _status_timeout: None,
> - }
> - }
> -
> - fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
> - match msg {
> - Msg::ReloadRrd => {
> - self._timeout = None;
> - let props = ctx.props();
> - let remote = props.remote.clone();
> - let node = props.node.clone();
> - let timeframe = self.rrd_time_frame;
> - self.async_pool.send_future(ctx.link().clone(), async move {
> - Self::reload_rrd(&remote, &node, timeframe).await
> - });
> - }
> - Msg::ReloadStatus => {
> - self._status_timeout = None;
> - let props = ctx.props();
> - let remote = props.remote.clone();
> - let node = props.node.clone();
> - self.async_pool.send_future(ctx.link().clone(), async move {
> - let res = Self::reload_status(&remote, &node).await;
> - Msg::StatusLoadFinished(res)
> - });
> - }
> - Msg::LoadFinished(res) => match res {
> - Ok(data_points) => {
> - self.last_error = None;
> - let mut cpu_vec = Vec::with_capacity(data_points.len());
> - let mut load_vec = Vec::with_capacity(data_points.len());
> - let mut mem_vec = Vec::with_capacity(data_points.len());
> - let mut mem_total_vec = Vec::with_capacity(data_points.len());
> - let mut time_vec = Vec::with_capacity(data_points.len());
> - for data in data_points {
> - cpu_vec.push(data.cpu_current.unwrap_or(f64::NAN));
> - load_vec.push(data.cpu_avg1.unwrap_or(f64::NAN));
> - mem_vec.push(data.mem_used.unwrap_or(f64::NAN));
> - mem_total_vec.push(data.mem_total.unwrap_or(f64::NAN));
> - time_vec.push(data.time as i64);
> - }
> -
> - self.cpu_data = Rc::new(Series::new(tr!("CPU"), cpu_vec));
> - self.load_data = Rc::new(Series::new(tr!("Server Load"), load_vec));
> - self.mem_data = Rc::new(Series::new(tr!("Used Memory"), mem_vec));
> - self.mem_total_data = Rc::new(Series::new(tr!("Total Memory"), mem_total_vec));
> - self.time_data = Rc::new(time_vec);
> -
> - let link = ctx.link().clone();
> - self._timeout = Some(gloo_timers::callback::Timeout::new(
> - ctx.props().rrd_interval,
> - move || link.send_message(Msg::ReloadRrd),
> - ))
> - }
> - Err(err) => self.last_error = Some(err),
> - },
> - Msg::StatusLoadFinished(res) => {
> - match res {
> - Ok(status) => {
> - self.last_status_error = None;
> - self.status = Some(status);
> - }
> - Err(err) => self.last_status_error = Some(err),
> - }
> - let link = ctx.link().clone();
> - self._status_timeout = Some(gloo_timers::callback::Timeout::new(
> - ctx.props().status_interval,
> - move || link.send_message(Msg::ReloadStatus),
> - ))
> - }
> - Msg::UpdateRrdTimeframe(rrd_time_frame) => {
> - self.rrd_time_frame = rrd_time_frame;
> - ctx.link().send_message(Msg::ReloadRrd);
> - return false;
> - }
> - }
> - true
> + fn create(_ctx: &yew::Context<Self>) -> Self {
> + Self
> }
>
> fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
> let props = ctx.props();
>
> if props.remote != old_props.remote || props.node != old_props.node {
> - self.status = None;
> - self.last_status_error = None;
> - self.last_error = None;
> - self.time_data = Rc::new(Vec::new());
> - self.cpu_data = Rc::new(Series::new("", Vec::new()));
> - self.load_data = Rc::new(Series::new("", Vec::new()));
> - self.mem_data = Rc::new(Series::new("", Vec::new()));
> - self.mem_total_data = Rc::new(Series::new("", Vec::new()));
> - self.async_pool = AsyncPool::new();
> - ctx.link()
> - .send_message_batch(vec![Msg::ReloadRrd, Msg::ReloadStatus]);
> true
> } else {
> false
> @@ -210,7 +58,8 @@ impl yew::Component for NodePanelComp {
> }
>
> fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
> - let props = ctx.props();
> + let props = ctx.props().clone();
> +
> let title: Html = Row::new()
> .gap(2)
> .class(AlignItems::Baseline)
> @@ -218,150 +67,17 @@ impl yew::Component for NodePanelComp {
> .with_child(tr! {"Node '{0}'", props.node})
> .into();
>
> - let mut status_comp = Column::new().gap(2).padding(4);
> - let status = self.status.as_ref();
> - let cpu = status.map(|s| s.cpu).unwrap_or_default();
> - let maxcpu = status.map(|s| s.cpuinfo.cpus).unwrap_or_default();
> - let load = status.map(|s| s.loadavg.join(", ")).unwrap_or_default();
> -
> - let memory = status.map(|s| s.memory.used as u64).unwrap_or_default();
> - let maxmem = status.map(|s| s.memory.total as u64).unwrap_or(1);
> -
> - let root = status.map(|s| s.rootfs.used as u64).unwrap_or_default();
> - let maxroot = status.map(|s| s.rootfs.total as u64).unwrap_or(1);
> -
> - let memory_used = memory as f64 / maxmem as f64;
> - let root_used = root as f64 / maxroot as f64;
> -
> - status_comp = status_comp
> - .with_child(make_row(
> - tr!("CPU usage"),
> - Fa::new("cpu"),
> - tr!("{0}% of {1} CPU(s)", format!("{:.2}", cpu * 100.0), maxcpu),
> - Some(cpu as f32),
> - ))
> - .with_child(make_row(
> - tr!("Load average"),
> - Fa::new("line-chart"),
> - load,
> - None,
> - ))
> - .with_child(make_row(
> - tr!("Memory usage"),
> - Fa::new("memory"),
> - tr!(
> - "{0}% ({1} of {2})",
> - format!("{:.2}", memory_used * 100.0),
> - HumanByte::from(memory),
> - HumanByte::from(maxmem),
> - ),
> - Some(memory_used as f32),
> - ))
> - .with_child(make_row(
> - tr!("Root filesystem usage"),
> - Fa::new("database"),
> - tr!(
> - "{0}% ({1} of {2})",
> - format!("{:.2}", root_used * 100.0),
> - HumanByte::from(root),
> - HumanByte::from(maxroot),
> - ),
> - Some(root_used as f32),
> - ))
> - .with_child(Container::new().padding(1)) // spacer
> - .with_child(
> - Row::new()
> - .with_child(tr!("Version"))
> - .with_flex_spacer()
> - .with_optional_child(status.map(|s| s.pveversion.as_str())),
> - )
> - .with_child(
> - Row::new()
> - .with_child(tr!("CPU Model"))
> - .with_flex_spacer()
> - .with_child(tr!(
> - "{0} ({1} sockets)",
> - status.map(|s| s.cpuinfo.model.as_str()).unwrap_or_default(),
> - status.map(|s| s.cpuinfo.sockets).unwrap_or_default()
> - )),
> - );
> -
> - if let Some(err) = &self.last_status_error {
> - status_comp.add_child(error_message(&err.to_string()));
> - }
> -
> - let loading = self.status.is_none() && self.last_status_error.is_none();
> - Panel::new()
> - .class(FlexFit)
> + TabPanel::new()
> + .class(pwt::css::FlexFit)
> .title(title)
> .class(ColorScheme::Neutral)
> - .with_child(
> - // FIXME: add some 'visible' or 'active' property to the progress
> - Progress::new()
> - .value((!loading).then_some(0.0))
> - .style("opacity", (!loading).then_some("0")),
> - )
> - .with_child(status_comp)
> - .with_child(separator().padding_x(4))
> - .with_child(
> - Row::new()
> - .padding_x(4)
> - .padding_y(1)
> - .class(JustifyContent::FlexEnd)
> - .with_child(
> - RRDTimeframeSelector::new()
> - .on_change(ctx.link().callback(Msg::UpdateRrdTimeframe)),
> - ),
> - )
> - .with_child(
> - Container::new().class(FlexFit).with_child(
> - Column::new()
> - .padding(4)
> - .gap(4)
> - .with_child(
> - RRDGraph::new(self.time_data.clone())
> - .title(tr!("CPU Usage"))
> - .render_value(|v: &f64| {
> - if v.is_finite() {
> - format!("{:.2}%", v * 100.0)
> - } else {
> - v.to_string()
> - }
> - })
> - .serie0(Some(self.cpu_data.clone())),
> - )
> - .with_child(
> - RRDGraph::new(self.time_data.clone())
> - .title(tr!("Server load"))
> - .render_value(|v: &f64| {
> - if v.is_finite() {
> - format!("{:.2}", v)
> - } else {
> - v.to_string()
> - }
> - })
> - .serie0(Some(self.load_data.clone())),
> - )
> - .with_child(
> - RRDGraph::new(self.time_data.clone())
> - .title(tr!("Memory Usage"))
> - .binary(true)
> - .render_value(|v: &f64| {
> - if v.is_finite() {
> - proxmox_human_byte::HumanByte::from(*v as u64).to_string()
> - } else {
> - v.to_string()
> - }
> - })
> - .serie0(Some(self.mem_total_data.clone()))
> - .serie1(Some(self.mem_data.clone())),
> - ),
> - ),
> + .with_item_builder(
> + TabBarItem::new()
> + .key("status_view")
> + .label(tr!("Overview"))
> + .icon_class("fa fa-tachometer"),
> + move |_| NodeOverviewPanel::new(props.remote.clone(), props.node.clone()).into(),
> )
> .into()
> }
> }
> -
> -fn make_row(title: String, icon: Fa, text: String, meter_value: Option<f32>) -> Column {
> - crate::renderer::status_row(title, icon, text, meter_value, false)
> -}
> diff --git a/ui/src/pve/node/overview.rs b/ui/src/pve/node/overview.rs
> new file mode 100644
> index 00000000..9ebc3e1d
> --- /dev/null
> +++ b/ui/src/pve/node/overview.rs
> @@ -0,0 +1,358 @@
> +use std::rc::Rc;
> +
> +use yew::{
> + virtual_dom::{VComp, VNode},
> + Context,
> +};
> +
> +use proxmox_human_byte::HumanByte;
> +use proxmox_yew_comp::{RRDGraph, RRDTimeframe, RRDTimeframeSelector, Series};
> +use pwt::{
> + css::{ColorScheme, FlexFit, JustifyContent},
> + prelude::*,
> + props::{ContainerBuilder, WidgetBuilder},
> + widget::{error_message, Column, Container, Fa, Progress, Row},
> + AsyncPool,
> +};
> +
> +use pdm_api_types::rrddata::NodeDataPoint;
> +use pdm_client::types::NodeStatus;
> +
> +use crate::renderer::separator;
> +
> +#[derive(Clone, Debug, Eq, PartialEq, Properties)]
> +pub struct NodeOverviewPanel {
> + /// The remote to show
> + pub remote: String,
> +
> + /// The node to show
> + pub node: String,
> +
> + #[prop_or(60_000)]
> + /// The interval for refreshing the rrd data
> + pub rrd_interval: u32,
> +
> + #[prop_or(10_000)]
> + /// The interval for refreshing the status data
> + pub status_interval: u32,
> +}
> +
> +impl NodeOverviewPanel {
> + pub fn new(remote: String, node: String) -> Self {
> + yew::props!(Self { remote, node })
> + }
> +}
> +
> +impl Into<VNode> for NodeOverviewPanel {
> + fn into(self) -> VNode {
> + VComp::new::<NodeOverviewPanelComp>(Rc::new(self), None).into()
> + }
> +}
> +
> +pub enum Msg {
> + ReloadRrd,
> + ReloadStatus,
> + LoadFinished(Result<Vec<NodeDataPoint>, proxmox_client::Error>),
> + StatusLoadFinished(Result<NodeStatus, proxmox_client::Error>),
> + UpdateRrdTimeframe(RRDTimeframe),
> +}
> +
> +pub struct NodeOverviewPanelComp {
> + time_data: Rc<Vec<i64>>,
> + cpu_data: Rc<Series>,
> + load_data: Rc<Series>,
> + mem_data: Rc<Series>,
> + mem_total_data: Rc<Series>,
> + status: Option<NodeStatus>,
> +
> + rrd_time_frame: RRDTimeframe,
> +
> + last_error: Option<proxmox_client::Error>,
> + last_status_error: Option<proxmox_client::Error>,
> +
> + async_pool: AsyncPool,
> + _timeout: Option<gloo_timers::callback::Timeout>,
> + _status_timeout: Option<gloo_timers::callback::Timeout>,
> +}
> +
> +impl NodeOverviewPanelComp {
> + async fn reload_rrd(remote: &str, node: &str, rrd_time_frame: RRDTimeframe) -> Msg {
> + let res = crate::pdm_client()
> + .pve_node_rrddata(remote, node, rrd_time_frame.mode, rrd_time_frame.timeframe)
> + .await;
> +
> + Msg::LoadFinished(res)
> + }
> +
> + async fn reload_status(remote: &str, node: &str) -> Result<NodeStatus, proxmox_client::Error> {
> + let status = crate::pdm_client().pve_node_status(remote, node).await?;
> + Ok(status)
> + }
> +}
> +
> +impl yew::Component for NodeOverviewPanelComp {
> + type Message = Msg;
> + type Properties = NodeOverviewPanel;
> +
> + fn create(ctx: &yew::Context<Self>) -> Self {
> + ctx.link().send_message(Msg::ReloadRrd);
> + ctx.link().send_message(Msg::ReloadStatus);
> + Self {
> + time_data: Rc::new(Vec::new()),> + cpu_data: Rc::new(Series::new("", Vec::new())),
> + load_data: Rc::new(Series::new("", Vec::new())),
> + mem_data: Rc::new(Series::new("", Vec::new())),
> + mem_total_data: Rc::new(Series::new("", Vec::new())),
pre-existing but this could profit from a Default implementation
> + rrd_time_frame: RRDTimeframe::load(),
> + status: None,
> + last_error: None,
> + last_status_error: None,
> + async_pool: AsyncPool::new(),
> + _timeout: None,
> + _status_timeout: None,
> + }
> + }
> +
> + fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
> + match msg {
> + Msg::ReloadRrd => {
> + self._timeout = None;
> + let props = ctx.props();
> + let remote = props.remote.clone();
> + let node = props.node.clone();
> + let timeframe = self.rrd_time_frame;
> + self.async_pool.send_future(ctx.link().clone(), async move {
> + Self::reload_rrd(&remote, &node, timeframe).await
> + });
> + }
> + Msg::ReloadStatus => {
> + self._status_timeout = None;
> + let props = ctx.props();
> + let remote = props.remote.clone();
> + let node = props.node.clone();
> + self.async_pool.send_future(ctx.link().clone(), async move {
> + let res = Self::reload_status(&remote, &node).await;
> + Msg::StatusLoadFinished(res)
> + });
> + }
> + Msg::LoadFinished(res) => match res {
> + Ok(data_points) => {
> + self.last_error = None;
> + let mut cpu_vec = Vec::with_capacity(data_points.len());
> + let mut load_vec = Vec::with_capacity(data_points.len());
> + let mut mem_vec = Vec::with_capacity(data_points.len());
> + let mut mem_total_vec = Vec::with_capacity(data_points.len());
> + let mut time_vec = Vec::with_capacity(data_points.len());
> + for data in data_points {
> + cpu_vec.push(data.cpu_current.unwrap_or(f64::NAN));
> + load_vec.push(data.cpu_avg1.unwrap_or(f64::NAN));
> + mem_vec.push(data.mem_used.unwrap_or(f64::NAN));
> + mem_total_vec.push(data.mem_total.unwrap_or(f64::NAN));
> + time_vec.push(data.time as i64);
> + }
> +
> + self.cpu_data = Rc::new(Series::new(tr!("CPU"), cpu_vec));
> + self.load_data = Rc::new(Series::new(tr!("Server Load"), load_vec));
> + self.mem_data = Rc::new(Series::new(tr!("Used Memory"), mem_vec));
> + self.mem_total_data = Rc::new(Series::new(tr!("Total Memory"), mem_total_vec));
> + self.time_data = Rc::new(time_vec);
> +
> + let link = ctx.link().clone();
> + self._timeout = Some(gloo_timers::callback::Timeout::new(
> + ctx.props().rrd_interval,
> + move || link.send_message(Msg::ReloadRrd),
> + ))
> + }
> + Err(err) => self.last_error = Some(err),
> + },
> + Msg::StatusLoadFinished(res) => {
> + match res {
> + Ok(status) => {
> + self.last_status_error = None;
> + self.status = Some(status);
> + }
> + Err(err) => self.last_status_error = Some(err),
> + }
> + let link = ctx.link().clone();
> + self._status_timeout = Some(gloo_timers::callback::Timeout::new(
> + ctx.props().status_interval,
> + move || link.send_message(Msg::ReloadStatus),
> + ))
> + }
> + Msg::UpdateRrdTimeframe(rrd_time_frame) => {
> + self.rrd_time_frame = rrd_time_frame;
> + ctx.link().send_message(Msg::ReloadRrd);
> + return false;
> + }
> + }
> + true
> + }
> +
> + fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
> + let props = ctx.props();
> +
> + if props.remote != old_props.remote || props.node != old_props.node {
> + self.status = None;
> + self.last_status_error = None;
> + self.last_error = None;
> + self.time_data = Rc::new(Vec::new());
> + self.cpu_data = Rc::new(Series::new("", Vec::new()));
> + self.load_data = Rc::new(Series::new("", Vec::new()));
> + self.mem_data = Rc::new(Series::new("", Vec::new()));
> + self.mem_total_data = Rc::new(Series::new("", Vec::new()));
> + self.async_pool = AsyncPool::new();
> + ctx.link()
> + .send_message_batch(vec![Msg::ReloadRrd, Msg::ReloadStatus]);
> + true
> + } else {
> + false
> + }
> + }
> +
> + fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
> + let mut status_comp = Column::new().gap(2).padding(4);
> + let status = self.status.as_ref();
> + let cpu = status.map(|s| s.cpu).unwrap_or_default();
> + let maxcpu = status.map(|s| s.cpuinfo.cpus).unwrap_or_default();
> + let load = status.map(|s| s.loadavg.join(", ")).unwrap_or_default();
> +
> + let memory = status.map(|s| s.memory.used as u64).unwrap_or_default();
> + let maxmem = status.map(|s| s.memory.total as u64).unwrap_or(1);
> +
> + let root = status.map(|s| s.rootfs.used as u64).unwrap_or_default();
> + let maxroot = status.map(|s| s.rootfs.total as u64).unwrap_or(1);
> +
> + let memory_used = memory as f64 / maxmem as f64;
> + let root_used = root as f64 / maxroot as f64;
> +
> + status_comp = status_comp
> + .with_child(make_row(
> + tr!("CPU usage"),
> + Fa::new("cpu"),
> + tr!("{0}% of {1} CPU(s)", format!("{:.2}", cpu * 100.0), maxcpu),
> + Some(cpu as f32),
> + ))
> + .with_child(make_row(
> + tr!("Load average"),
> + Fa::new("line-chart"),
> + load,
> + None,
> + ))
> + .with_child(make_row(
> + tr!("Memory usage"),
> + Fa::new("memory"),
> + tr!(
> + "{0}% ({1} of {2})",
> + format!("{:.2}", memory_used * 100.0),
> + HumanByte::from(memory),
> + HumanByte::from(maxmem),
> + ),
> + Some(memory_used as f32),
> + ))
> + .with_child(make_row(
> + tr!("Root filesystem usage"),
> + Fa::new("database"),
> + tr!(
> + "{0}% ({1} of {2})",
> + format!("{:.2}", root_used * 100.0),
> + HumanByte::from(root),
> + HumanByte::from(maxroot),
> + ),
> + Some(root_used as f32),
> + ))
> + .with_child(Container::new().padding(1)) // spacer
> + .with_child(
> + Row::new()
> + .with_child(tr!("Version"))
> + .with_flex_spacer()
> + .with_optional_child(status.map(|s| s.pveversion.as_str())),
> + )
> + .with_child(
> + Row::new()
> + .with_child(tr!("CPU Model"))
> + .with_flex_spacer()
> + .with_child(tr!(
> + "{0} ({1} sockets)",
> + status.map(|s| s.cpuinfo.model.as_str()).unwrap_or_default(),
> + status.map(|s| s.cpuinfo.sockets).unwrap_or_default()
> + )),
> + );
> +
> + if let Some(err) = &self.last_status_error {
> + status_comp.add_child(error_message(&err.to_string()));
> + }
> +
> + let loading = self.status.is_none() && self.last_status_error.is_none();
> + Container::new()
> + .class(FlexFit)
> + .class(ColorScheme::Neutral)
> + .with_child(
> + // FIXME: add some 'visible' or 'active' property to the progress
> + Progress::new()
> + .value((!loading).then_some(0.0))
> + .style("opacity", (!loading).then_some("0")),
> + )
> + .with_child(status_comp)
> + .with_child(separator().padding_x(4))
> + .with_child(
> + Row::new()
> + .padding_x(4)
> + .padding_y(1)
> + .class(JustifyContent::FlexEnd)
> + .with_child(
> + RRDTimeframeSelector::new()
> + .on_change(ctx.link().callback(Msg::UpdateRrdTimeframe)),
> + ),
> + )
> + .with_child(
> + Container::new().class(FlexFit).with_child(
> + Column::new()
> + .padding(4)
> + .gap(4)
> + .with_child(
> + RRDGraph::new(self.time_data.clone())
> + .title(tr!("CPU Usage"))
> + .render_value(|v: &f64| {
> + if v.is_finite() {
> + format!("{:.2}%", v * 100.0)
> + } else {
> + v.to_string()
> + }
> + })
> + .serie0(Some(self.cpu_data.clone())),
> + )
> + .with_child(
> + RRDGraph::new(self.time_data.clone())
> + .title(tr!("Server load"))
> + .render_value(|v: &f64| {
> + if v.is_finite() {
> + format!("{:.2}", v)
> + } else {
> + v.to_string()
> + }
> + })
> + .serie0(Some(self.load_data.clone())),
> + )
> + .with_child(
> + RRDGraph::new(self.time_data.clone())
> + .title(tr!("Memory Usage"))
> + .binary(true)
> + .render_value(|v: &f64| {
> + if v.is_finite() {
> + proxmox_human_byte::HumanByte::from(*v as u64).to_string()
> + } else {
> + v.to_string()
> + }
> + })
> + .serie0(Some(self.mem_total_data.clone()))
> + .serie1(Some(self.mem_data.clone())),
> + ),
> + ),
> + )
> + .into()
> + }
> +}
> +
> +fn make_row(title: String, icon: Fa, text: String, meter_value: Option<f32>) -> Column {
> + crate::renderer::status_row(title, icon, text, meter_value, false)
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes
2025-09-03 9:02 ` Stefan Hanreich
@ 2025-09-03 9:19 ` Stefan Hanreich
2025-09-03 9:23 ` Lukas Wagner
1 sibling, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2025-09-03 9:19 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Lukas Wagner
On 9/3/25 11:02 AM, Stefan Hanreich wrote:
> On 9/2/25 5:14 PM, Lukas Wagner wrote:
>> This adds new APIs for update management:
>>
>> GET /pve/remotes/{remote}/nodes/{node}/apt/changelog
>> -> get package changelog
>> GET /pve/remotes/{remote}/nodes/{node}/apt/update
>> -> get list of updatable packages
>> POST /pve/remotes/{remote}/nodes/{node}/apt/update
>> -> refresh APT database
>>
>> At this time these just pass the call through to PVE with no caching
>> involved on the PDM side. This should be fine for this API, but once
>> we have an API for 'give me a view of ALL available remote updates',
>> we need to introduce a cache that is periodically refreshed.
>>
>> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
>> ---
>> server/src/api/pve/apt.rs | 119 +++++++++++++++++++++++++++++++++++
>> server/src/api/pve/mod.rs | 3 +-
>> server/src/api/pve/node.rs | 1 +
>> server/src/lib.rs | 1 +
>> server/src/remote_updates.rs | 96 ++++++++++++++++++++++++++++
>> 5 files changed, 219 insertions(+), 1 deletion(-)
>> create mode 100644 server/src/api/pve/apt.rs
>> create mode 100644 server/src/remote_updates.rs
>>
>> diff --git a/server/src/api/pve/apt.rs b/server/src/api/pve/apt.rs
>> new file mode 100644
>> index 00000000..f5027fb8
>> --- /dev/null
>> +++ b/server/src/api/pve/apt.rs
>> @@ -0,0 +1,119 @@
>> +use anyhow::Error;
>> +
>> +use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
>> +use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
>> +use proxmox_schema::api;
>> +use proxmox_schema::api_types::NODE_SCHEMA;
>> +
>> +use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, RemoteUpid, PRIV_RESOURCE_MODIFY};
>> +
>> +use crate::{api::remotes::get_remote, remote_updates};
>> +
>> +#[api(
>> + input: {
>> + properties: {
>> + remote: {
>> + schema: REMOTE_ID_SCHEMA,
>> + },
>> + node: {
>> + schema: NODE_SCHEMA,
>> + },
>> + },
>> + },
>> + returns: {
>> + description: "A list of packages with available updates.",
>> + type: Array,
>> + items: {
>> + type: APTUpdateInfo
>> + },
>> + },
>> + access: {
>> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
>> + },
>> +)]
>> +/// List available APT updates for a remote PVE node.
>> +async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
>
> potentially fine to take a ref here for both params as well?
>
>> + let (config, _digest) = pdm_config::remotes::config()?;
>> + let remote = get_remote(&config, &remote)?;
>> +
>> + let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
>> +
>> + Ok(updates)
>> +}
>> +
>> +#[api(
>> + input: {
>> + properties: {
>> + remote: {
>> + schema: REMOTE_ID_SCHEMA,
>> + },
>> + node: {
>> + schema: NODE_SCHEMA,
>> + },
>> + },
>> + },
>> + access: {
>> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
>> + },
>> +)]
>> +/// Update the APT database of a remote PVE node.
>> +pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
>
> here too then
>
>> + let (config, _digest) = pdm_config::remotes::config()?;
>> + let remote = get_remote(&config, &remote)?;
>> +
>> + let upid = remote_updates::update_apt_database(remote, &node).await?;
>> +
>> + Ok(upid)
>> +}
>> +
>> +#[api(
>> + input: {
>> + properties: {
>> + remote: {
>> + schema: REMOTE_ID_SCHEMA,
>> + },
>> + node: {
>> + schema: NODE_SCHEMA,
>> + },
>> + options: {
>> + type: APTGetChangelogOptions,
>> + flatten: true,
>> + },
>> + },
>> + },
>> + returns: {
>> + description: "The Package changelog.",
>> + type: String,
>> + },
>> + access: {
>> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
>> + },
>> +)]
>> +/// Retrieve the changelog of the specified package for a remote PVE node.
>> +async fn apt_get_changelog(
>> + remote: String,
>> + node: String,
>> + options: APTGetChangelogOptions,
>
> here too then
sorry, was too liberal when scanning the functions, those three cannot
take a ref ofc
[snip]
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes
2025-09-03 9:02 ` Stefan Hanreich
2025-09-03 9:19 ` Stefan Hanreich
@ 2025-09-03 9:23 ` Lukas Wagner
1 sibling, 0 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-03 9:23 UTC (permalink / raw)
To: Stefan Hanreich, Proxmox Datacenter Manager development discussion
On Wed Sep 3, 2025 at 11:02 AM CEST, Stefan Hanreich wrote:
> On 9/2/25 5:14 PM, Lukas Wagner wrote:
>> This adds new APIs for update management:
>>
>> GET /pve/remotes/{remote}/nodes/{node}/apt/changelog
>> -> get package changelog
>> GET /pve/remotes/{remote}/nodes/{node}/apt/update
>> -> get list of updatable packages
>> POST /pve/remotes/{remote}/nodes/{node}/apt/update
>> -> refresh APT database
>>
>> At this time these just pass the call through to PVE with no caching
>> involved on the PDM side. This should be fine for this API, but once
>> we have an API for 'give me a view of ALL available remote updates',
>> we need to introduce a cache that is periodically refreshed.
>>
>> Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
>> ---
>> server/src/api/pve/apt.rs | 119 +++++++++++++++++++++++++++++++++++
>> server/src/api/pve/mod.rs | 3 +-
>> server/src/api/pve/node.rs | 1 +
>> server/src/lib.rs | 1 +
>> server/src/remote_updates.rs | 96 ++++++++++++++++++++++++++++
>> 5 files changed, 219 insertions(+), 1 deletion(-)
>> create mode 100644 server/src/api/pve/apt.rs
>> create mode 100644 server/src/remote_updates.rs
>>
>> diff --git a/server/src/api/pve/apt.rs b/server/src/api/pve/apt.rs
>> new file mode 100644
>> index 00000000..f5027fb8
>> --- /dev/null
>> +++ b/server/src/api/pve/apt.rs
>> @@ -0,0 +1,119 @@
>> +use anyhow::Error;
>> +
>> +use proxmox_apt_api_types::{APTGetChangelogOptions, APTUpdateInfo};
>> +use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
>> +use proxmox_schema::api;
>> +use proxmox_schema::api_types::NODE_SCHEMA;
>> +
>> +use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, RemoteUpid, PRIV_RESOURCE_MODIFY};
>> +
>> +use crate::{api::remotes::get_remote, remote_updates};
>> +
>> +#[api(
>> + input: {
>> + properties: {
>> + remote: {
>> + schema: REMOTE_ID_SCHEMA,
>> + },
>> + node: {
>> + schema: NODE_SCHEMA,
>> + },
>> + },
>> + },
>> + returns: {
>> + description: "A list of packages with available updates.",
>> + type: Array,
>> + items: {
>> + type: APTUpdateInfo
>> + },
>> + },
>> + access: {
>> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
>> + },
>> +)]
>> +/// List available APT updates for a remote PVE node.
>> +async fn apt_update_available(remote: String, node: String) -> Result<Vec<APTUpdateInfo>, Error> {
>
> potentially fine to take a ref here for both params as well?
Using a &str here leads to a compiler error
error: implementation of `Deserialize` is not general enough
>
>> + let (config, _digest) = pdm_config::remotes::config()?;
>> + let remote = get_remote(&config, &remote)?;
>> +
>> + let updates = remote_updates::list_available_updates(remote.clone(), &node).await?;
>> +
>> + Ok(updates)
>> +}
>> +
>> +#[api(
>> + input: {
>> + properties: {
>> + remote: {
>> + schema: REMOTE_ID_SCHEMA,
>> + },
>> + node: {
>> + schema: NODE_SCHEMA,
>> + },
>> + },
>> + },
>> + access: {
>> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
>> + },
>> +)]
>> +/// Update the APT database of a remote PVE node.
>> +pub async fn apt_update_database(remote: String, node: String) -> Result<RemoteUpid, Error> {
>
> here too then
Same here.
>> + let (config, _digest) = pdm_config::remotes::config()?;
>> + let remote = get_remote(&config, &remote)?;
>> +
>> + let upid = remote_updates::update_apt_database(remote, &node).await?;
>> +
>> + Ok(upid)
>> +}
>> +
>> +#[api(
>> + input: {
>> + properties: {
>> + remote: {
>> + schema: REMOTE_ID_SCHEMA,
>> + },
>> + node: {
>> + schema: NODE_SCHEMA,
>> + },
>> + options: {
>> + type: APTGetChangelogOptions,
>> + flatten: true,
>> + },
>> + },
>> + },
>> + returns: {
>> + description: "The Package changelog.",
>> + type: String,
>> + },
>> + access: {
>> + permission: &Permission::Privilege(&["resource", "{remote}", "node", "{node}", "system"], PRIV_RESOURCE_MODIFY, false),
>> + },
>> +)]
>> +/// Retrieve the changelog of the specified package for a remote PVE node.
>> +async fn apt_get_changelog(
>> + remote: String,
>> + node: String,
>> + options: APTGetChangelogOptions,
>
> here too then
>
Same here.
>> +) -> Result<String, Error> {
>> + let (config, _digest) = pdm_config::remotes::config()?;
>> + let remote = get_remote(&config, &remote)?;
>> +
>> + remote_updates::get_changelog(remote.clone(), &node, options.name).await
>> +}
>> +
>> +const SUBDIRS: SubdirMap = &[
>> + (
>> + "changelog",
>> + &Router::new().get(&API_METHOD_APT_GET_CHANGELOG),
>> + ),
>> + (
>> + "update",
>> + &Router::new()
>> + .get(&API_METHOD_APT_UPDATE_AVAILABLE)
>> + .post(&API_METHOD_APT_UPDATE_DATABASE),
>> + ),
>> +];
>> +
>> +pub const ROUTER: Router = Router::new()
>> + .get(&list_subdirs_api_method!(SUBDIRS))
>> + .subdirs(SUBDIRS);
>> diff --git a/server/src/api/pve/mod.rs b/server/src/api/pve/mod.rs
>> index 2cfdc5b7..0768083d 100644
>> --- a/server/src/api/pve/mod.rs
>> +++ b/server/src/api/pve/mod.rs
>> @@ -31,6 +31,7 @@ use crate::connection::PveClient;
>> use crate::connection::{self, probe_tls_connection};
>> use crate::remote_tasks;
>>
>> +mod apt;
>> mod lxc;
>> mod node;
>> mod qemu;
>> @@ -77,7 +78,7 @@ const RESOURCES_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_RESOURCES
>> const STATUS_ROUTER: Router = Router::new().get(&API_METHOD_CLUSTER_STATUS);
>>
>> // converts a remote + PveUpid into a RemoteUpid and starts tracking it
>> -async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
>> +pub async fn new_remote_upid(remote: String, upid: PveUpid) -> Result<RemoteUpid, Error> {
>> let remote_upid: RemoteUpid = (remote, upid.to_string()).try_into()?;
>> remote_tasks::track_running_task(remote_upid.clone()).await?;
>> Ok(remote_upid)
>> diff --git a/server/src/api/pve/node.rs b/server/src/api/pve/node.rs
>> index df96a1c3..99539d1c 100644
>> --- a/server/src/api/pve/node.rs
>> +++ b/server/src/api/pve/node.rs
>> @@ -13,6 +13,7 @@ pub const ROUTER: Router = Router::new()
>>
>> #[sortable]
>> const SUBDIRS: SubdirMap = &sorted!([
>> + ("apt", &super::apt::ROUTER),
>> ("rrddata", &super::rrddata::NODE_RRD_ROUTER),
>> ("network", &Router::new().get(&API_METHOD_GET_NETWORK)),
>> ("storage", &Router::new().get(&API_METHOD_GET_STORAGES)),
>> diff --git a/server/src/lib.rs b/server/src/lib.rs
>> index 3f8b7708..a58190d8 100644
>> --- a/server/src/lib.rs
>> +++ b/server/src/lib.rs
>> @@ -9,6 +9,7 @@ pub mod metric_collection;
>> pub mod parallel_fetcher;
>> pub mod remote_cache;
>> pub mod remote_tasks;
>> +pub mod remote_updates;
>> pub mod resource_cache;
>> pub mod task_utils;
>>
>> diff --git a/server/src/remote_updates.rs b/server/src/remote_updates.rs
>> new file mode 100644
>> index 00000000..809e5de1
>> --- /dev/null
>> +++ b/server/src/remote_updates.rs
>> @@ -0,0 +1,96 @@
>> +use anyhow::Error;
>> +use pdm_api_types::RemoteUpid;
>> +
>> +use proxmox_apt_api_types::APTUpdateInfo;
>> +
>> +use pdm_api_types::remotes::{Remote, RemoteType};
>> +
>> +use crate::api::pve::new_remote_upid;
>> +use crate::connection;
>> +
>> +/// Return a list of available updates for a given remote node.
>> +pub async fn list_available_updates(
>> + remote: Remote,
>> + node: &str,
>> +) -> Result<Vec<APTUpdateInfo>, Error> {
>> + let updates = fetch_available_updates(remote, node.to_string()).await?;
>> + Ok(updates)
>> +}
>> +
>
> Is there a reason for having this as an additional function and not just
> calling fetch_available_updates at its callsites (after changing its
> visibility)?
>
> At the very least it could return the result of the await directly? Or
> do you want to reserve the option of changing the public API? In that
> case we could still introduce a private helper later.
>
This is basically a left-over from some other patches I was already
working on but couldn't get ready in time before my vacation.
In the future there will be some periodic task that will fetch available
updates from all nodes and put the results in a cache. The
fetch_available_udpates was the function used by
ParallelFetcher::for_all_remote_nodes to do the fetching.
I'd leave it for now if you don't mind.
>> +/// Trigger `apt update` on a remote node.
>> +///
>> +/// The function returns a `[RemoteUpid]` for the started update task.
>> +pub async fn update_apt_database(remote: &Remote, node: &str) -> Result<RemoteUpid, Error> {
>> + match remote.ty {
>> + RemoteType::Pve => {
>> + let client = connection::make_pve_client(remote)?;
>> +
>> + let params = pve_api_types::AptUpdateParams {
>> + notify: Some(false),
>> + quiet: Some(false),
>> + };
>> + let upid = client.update_apt_database(node, params).await?;
>> +
>> + new_remote_upid(remote.id.clone(), upid).await
>> + }
>> + RemoteType::Pbs => todo!(),
>
> return an Error here?
>
Will do, as noted in my other mail!
>> + }
>> +}
>> +
>> +/// Get the changelog for a given package.
>> +pub async fn get_changelog(remote: Remote, node: &str, package: String) -> Result<String, Error> {
>
> Interestingly the generated binding takes a String even though it
> wouldn't need to? Maybe something we should check in the generation
> logic later?
>
>> + match remote.ty {
>> + RemoteType::Pve => {
>> + let client = connection::make_pve_client(&remote)?;
>> +
>> + client
>> + .get_package_changelog(node, package, None)
>> + .await
>> + .map_err(Into::into)
>> + }
>> + RemoteType::Pbs => Ok("TODO: Return PBS package changelog".into()),
>> + }
>> +}
>> +
>> +async fn fetch_available_updates(
>> + remote: Remote,
>> + node: String,
>
> take a ref here too?
>
As explained above, this one will be used by ParallelFetcher later,
which requires the owned params. Sorry, maybe I should've put that in a
comment; I was kind of in a hurry yesterday to get these patches out in
time.
>> +) -> Result<Vec<APTUpdateInfo>, Error> {
>> + match remote.ty {
>> + RemoteType::Pve => {
>> + let client = connection::make_pve_client(&remote)?;
>> +
>> + let updates = client
>> + .list_available_updates(&node)
>> + .await?
>> + .into_iter()
>> + .map(map_pve_update_info)
>> + .collect();
>> +
>> + Ok(updates)
>> + }
>> + RemoteType::Pbs => {
>> + let _client = connection::make_pbs_client(&remote)?;
>> +
>> + // TODO: Fetch available updates
>> + //
>> +
>> + Ok(Vec::new())
>> + }
>> + }
>> +}
>> +
>> +fn map_pve_update_info(info: pve_api_types::AptUpdateInfo) -> APTUpdateInfo {
>> + APTUpdateInfo {
>> + package: info.package,
>> + title: info.title,
>> + arch: info.arch,
>> + description: info.description,
>> + version: info.version,
>> + old_version: info.old_version.unwrap_or_default(),
>> + origin: info.origin,
>> + priority: info.priority,
>> + section: info.section,
>> + extra_info: None,
>> + }
>> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab
2025-09-03 9:10 ` Stefan Hanreich
@ 2025-09-03 9:48 ` Lukas Wagner
2025-09-03 9:52 ` Stefan Hanreich
0 siblings, 1 reply; 25+ messages in thread
From: Lukas Wagner @ 2025-09-03 9:48 UTC (permalink / raw)
To: Stefan Hanreich, Proxmox Datacenter Manager development discussion
On Wed Sep 3, 2025 at 11:10 AM CEST, Stefan Hanreich wrote:
> one small nit inline (pre-existing) I noticed while looking over the changes
>
>> +impl yew::Component for NodeOverviewPanelComp {
>> + type Message = Msg;
>> + type Properties = NodeOverviewPanel;
>> +
>> + fn create(ctx: &yew::Context<Self>) -> Self {
>> + ctx.link().send_message(Msg::ReloadRrd);
>> + ctx.link().send_message(Msg::ReloadStatus);
>> + Self {
>> + time_data: Rc::new(Vec::new()),> + cpu_data: Rc::new(Series::new("", Vec::new())),
>> + load_data: Rc::new(Series::new("", Vec::new())),
>> + mem_data: Rc::new(Series::new("", Vec::new())),
>> + mem_total_data: Rc::new(Series::new("", Vec::new())),
>
> pre-existing but this could profit from a Default implementation
>
Mhhm, does that make sense though? Default::default is off course
public, and it does not really make much sense to offer a second way to
instantiate the component that is not `create`... but maybe I'm missing
something here.
>> + rrd_time_frame: RRDTimeframe::load(),
>> + status: None,
>> + last_error: None,
>> + last_status_error: None,
>> + async_pool: AsyncPool::new(),
>> + _timeout: None,
>> + _status_timeout: None,
>> + }
>> + }
>> +
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab
2025-09-03 9:48 ` Lukas Wagner
@ 2025-09-03 9:52 ` Stefan Hanreich
2025-09-03 9:55 ` Lukas Wagner
0 siblings, 1 reply; 25+ messages in thread
From: Stefan Hanreich @ 2025-09-03 9:52 UTC (permalink / raw)
To: Lukas Wagner, Proxmox Datacenter Manager development discussion
On 9/3/25 11:47 AM, Lukas Wagner wrote:
> On Wed Sep 3, 2025 at 11:10 AM CEST, Stefan Hanreich wrote:
>> one small nit inline (pre-existing) I noticed while looking over the changes
>>
>>> +impl yew::Component for NodeOverviewPanelComp {
>>> + type Message = Msg;
>>> + type Properties = NodeOverviewPanel;
>>> +
>>> + fn create(ctx: &yew::Context<Self>) -> Self {
>>> + ctx.link().send_message(Msg::ReloadRrd);
>>> + ctx.link().send_message(Msg::ReloadStatus);
>>> + Self {
>>> + time_data: Rc::new(Vec::new()),> + cpu_data: Rc::new(Series::new("", Vec::new())),
>>> + load_data: Rc::new(Series::new("", Vec::new())),
>>> + mem_data: Rc::new(Series::new("", Vec::new())),
>>> + mem_total_data: Rc::new(Series::new("", Vec::new())),
>>
>> pre-existing but this could profit from a Default implementation
>>
>
> Mhhm, does that make sense though? Default::default is off course
> public, and it does not really make much sense to offer a second way to
> instantiate the component that is not `create`... but maybe I'm missing
> something here.
I meant Series itself, sorry if that wasn't clear. Then we could use
Default::default here for a lot of the fields.
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab
2025-09-03 9:52 ` Stefan Hanreich
@ 2025-09-03 9:55 ` Lukas Wagner
0 siblings, 0 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-03 9:55 UTC (permalink / raw)
To: Stefan Hanreich, Proxmox Datacenter Manager development discussion
On Wed Sep 3, 2025 at 11:52 AM CEST, Stefan Hanreich wrote:
> On 9/3/25 11:47 AM, Lukas Wagner wrote:
>> On Wed Sep 3, 2025 at 11:10 AM CEST, Stefan Hanreich wrote:
>>> one small nit inline (pre-existing) I noticed while looking over the changes
>>>
>>>> +impl yew::Component for NodeOverviewPanelComp {
>>>> + type Message = Msg;
>>>> + type Properties = NodeOverviewPanel;
>>>> +
>>>> + fn create(ctx: &yew::Context<Self>) -> Self {
>>>> + ctx.link().send_message(Msg::ReloadRrd);
>>>> + ctx.link().send_message(Msg::ReloadStatus);
>>>> + Self {
>>>> + time_data: Rc::new(Vec::new()),> + cpu_data: Rc::new(Series::new("", Vec::new())),
>>>> + load_data: Rc::new(Series::new("", Vec::new())),
>>>> + mem_data: Rc::new(Series::new("", Vec::new())),
>>>> + mem_total_data: Rc::new(Series::new("", Vec::new())),
>>>
>>> pre-existing but this could profit from a Default implementation
>>>
>>
>> Mhhm, does that make sense though? Default::default is off course
>> public, and it does not really make much sense to offer a second way to
>> instantiate the component that is not `create`... but maybe I'm missing
>> something here.
>
> I meant Series itself, sorry if that wasn't clear. Then we could use
> Default::default here for a lot of the fields.
Ah, got you.
Yes, that would of course make sense. Should be done in a separate
patch, I would suggest.
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* Re: [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (9 preceding siblings ...)
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/4] ui: pve: node: add update tab Lukas Wagner
@ 2025-09-03 10:20 ` Stefan Hanreich
2025-09-03 11:43 ` [pdm-devel] superseded: " Lukas Wagner
11 siblings, 0 replies; 25+ messages in thread
From: Stefan Hanreich @ 2025-09-03 10:20 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Lukas Wagner
gave this a quick spin on my PDM cluster and it seemed to work well.
Checked the available upgrades for all my nodes (PVE 8 and PVE 9). Then
upgraded a node and re-checked the output.
Some notes (all of them seem to be pre-existing to the current panel):
We might want to show a custom text if there are no upgrades available.
We might wanna think about improving the description column since in its
current form it shows a small part of the description truncated, which
isn't that useful imo. The title field might be better suited for
displaying in the table view?
Potentially add that double-clicking on a row opens a small window that
shows a detailed view with the information we get from the API already?
Since they're all pre-existing and probably better suited for a
follow-up, consider this series:
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
On 9/2/25 5:14 PM, Lukas Wagner wrote:
> This series adds a new 'Updates' tab for PVE remotes. The existing status
> overview is moved to a new 'Overview' tab, which is visible by default.
>
> On the backend side, we add a couple new API endpoints, which simply pass
> through the request to the PVE nodes, no caching for now.
>
> GET /pve/remotes/{remote}/nodes/{node}/apt
> Get list of updatable packages
> GET /pve/remotes/{remote}/nodes/{node}/changelog
> Get list of changelog of package
> POST /pve/remotes/{remote}/nodes/{node}/apt
> Update APT package database
>
> In terms of permissions, these new API endpoints require RESOURCE_MODIFY privs on
> /resource/{remote}/node/{node}/system
>
> This was the result of a short discussion in the development chat room.
>
> The existing APT view component is a bit large for this panel, maybe we could
> hide the package description by default (but not too important for now).
>
> Future work (some backend work already started, but can't finish before my
> vacation):
>
> - "Global Update" view that lists update status of all remote nodes
> - Cache update status per node (absolutely necessary for the 'global' view),
> with a task refreshing the update status every couple of hours
> - Maybe send a notification about the global update availabilty (require notification
> stack integration first)
> - Add new API functions to pdm-client crate and CLI
> - Allow package upgrade (requires web socket proxying, as far as I can see,
> haven't really looked into it much)
>
>
> pve-manager:
>
> Lukas Wagner (1):
> api: apt: add JSON schema for 'list_updates' endpoint
>
> PVE/API2/APT.pm | 46 +++++++++++++++++++++++++++++++++++++++++++++-
> 1 file changed, 45 insertions(+), 1 deletion(-)
>
>
> proxmox-api-types:
>
> Lukas Wagner (3):
> Schema2Rust: fix handling of non-optional params
> generate: add bindings for various APT functions
> refresh bindings
>
> pve-api-types/generate.pl | 4 +
> pve-api-types/generator-lib/Schema2Rust.pm | 9 +-
> pve-api-types/src/generated/code.rs | 58 ++++++++++-
> pve-api-types/src/generated/types.rs | 108 +++++++++++++++++++++
> 4 files changed, 175 insertions(+), 4 deletions(-)
>
>
> proxmox-yew-comp:
>
> Lukas Wagner (2):
> apt view: allow to set task_base_url
> apt view: reload if base urls have changed
>
> src/apt_package_manager.rs | 23 +++++++++++++++++++++++
> 1 file changed, 23 insertions(+)
>
>
> proxmox-datacenter-manager:
>
> Lukas Wagner (4):
> server: add api for getting available updates/changelogs for remote
> nodes
> ui: pve: promote node.rs to dir-style module
> ui: pve: move node overview to a new overview tab
> ui: pve: node: add update tab
>
> server/src/api/pve/apt.rs | 119 +++++++++++++++++++++++
> server/src/api/pve/mod.rs | 3 +-
> server/src/api/pve/node.rs | 1 +
> server/src/lib.rs | 1 +
> server/src/remote_updates.rs | 96 ++++++++++++++++++
> ui/src/pve/node/mod.rs | 103 ++++++++++++++++++++
> ui/src/pve/{node.rs => node/overview.rs} | 31 +++---
> 7 files changed, 333 insertions(+), 21 deletions(-)
> create mode 100644 server/src/api/pve/apt.rs
> create mode 100644 server/src/remote_updates.rs
> create mode 100644 ui/src/pve/node/mod.rs
> rename ui/src/pve/{node.rs => node/overview.rs} (95%)
>
>
> Summary over all repositories:
> 13 files changed, 576 insertions(+), 26 deletions(-)
>
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
* [pdm-devel] superseded: [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
` (10 preceding siblings ...)
2025-09-03 10:20 ` [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Stefan Hanreich
@ 2025-09-03 11:43 ` Lukas Wagner
11 siblings, 0 replies; 25+ messages in thread
From: Lukas Wagner @ 2025-09-03 11:43 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion; +Cc: pdm-devel
On Tue Sep 2, 2025 at 5:14 PM CEST, Lukas Wagner wrote:
> This series adds a new 'Updates' tab for PVE remotes. The existing status
> overview is moved to a new 'Overview' tab, which is visible by default.
>
> On the backend side, we add a couple new API endpoints, which simply pass
> through the request to the PVE nodes, no caching for now.
>
superseded by v2:
https://lore.proxmox.com/pdm-devel/20250903114123.215787-1-l.wagner@proxmox.com/T/#t
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 25+ messages in thread
end of thread, other threads:[~2025-09-03 11:43 UTC | newest]
Thread overview: 25+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-09-02 15:14 [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH manager 1/1] api: apt: add JSON schema for 'list_updates' endpoint Lukas Wagner
2025-09-03 8:25 ` [pdm-devel] applied: " Thomas Lamprecht
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 1/3] Schema2Rust: fix handling of non-optional params Lukas Wagner
2025-09-03 8:57 ` [pdm-devel] applied: " Wolfgang Bumiller
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 2/3] generate: add bindings for various APT functions Lukas Wagner
2025-09-03 8:58 ` [pdm-devel] applied: " Wolfgang Bumiller
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-api-types 3/3] refresh bindings Lukas Wagner
2025-09-03 8:59 ` Wolfgang Bumiller
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-yew-comp 1/2] apt view: allow to set task_base_url Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-yew-comp 2/2] apt view: reload if base urls have changed Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 1/4] server: add api for getting available updates/changelogs for remote nodes Lukas Wagner
2025-09-03 8:42 ` Lukas Wagner
2025-09-03 9:02 ` Stefan Hanreich
2025-09-03 9:19 ` Stefan Hanreich
2025-09-03 9:23 ` Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 2/4] ui: pve: promote node.rs to dir-style module Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 3/4] ui: pve: move node overview to a new overview tab Lukas Wagner
2025-09-03 9:10 ` Stefan Hanreich
2025-09-03 9:48 ` Lukas Wagner
2025-09-03 9:52 ` Stefan Hanreich
2025-09-03 9:55 ` Lukas Wagner
2025-09-02 15:14 ` [pdm-devel] [PATCH proxmox-datacenter-manager 4/4] ui: pve: node: add update tab Lukas Wagner
2025-09-03 10:20 ` [pdm-devel] [PATCH manager/proxmox{-api-types, -yew-comp, -datacenter-manager} 00/10] PVE node update view Stefan Hanreich
2025-09-03 11:43 ` [pdm-devel] superseded: " Lukas Wagner
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox