* [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard
@ 2025-08-26 10:15 Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 1/8] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
` (9 more replies)
0 siblings, 10 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
This adds new panels to the dashboard for showing summaries of the tasks:
* one by category
* one by remote
similar to what we have in PBS.
I did not implement a cache for the data on the backend, but this can be
done as a follow up and should not impact the UI code at all.
changes from v3:
* instead of having freestanding helper functions, introduce a struct
with methods that we can pass around, this makes it easier to sort,
etc. also with this we can get rid of another helper type in the
tasks ui
* changed to a btreemap from a hashmap to keep the sorting we get from
the TaskWorkerType
* deduplicated the since/hour calculation in the dashboard
changes from v2:
* rebase on current master (incl task caching changes from lukas)
* new patch (8/8) that includes configuring the time frame to check the
tasks for
Changes from v1:
* incorporated most suggestions from Thomas
* split up patches a bit
* added new server api call
* (the UI code is very different to accomodate the necessary changes)
Dominik Csapak (8):
server: task cache: treat a limit of 0 as unbounded
server: api: remote tasks: add 'remote' filter option
server: api: add remote-tasks statistics
ui: refactor remote upid formatter
ui: tasks: add helper to summarize task categories
ui: add dialog to show filtered tasks
ui: dashboard: add task summaries
ui: dashboard: make task summary time range configurable
lib/pdm-api-types/src/lib.rs | 46 +++++
server/src/api/remote_tasks.rs | 88 ++++++++-
server/src/remote_tasks/mod.rs | 17 +-
ui/src/dashboard/filtered_tasks.rs | 291 ++++++++++++++++++++++++++++
ui/src/dashboard/mod.rs | 128 +++++++++++-
ui/src/dashboard/tasks.rs | 301 +++++++++++++++++++++++++++++
ui/src/remotes/tasks.rs | 19 +-
ui/src/tasks.rs | 103 +++++++++-
ui/src/top_nav_bar.rs | 18 +-
9 files changed, 966 insertions(+), 45 deletions(-)
create mode 100644 ui/src/dashboard/filtered_tasks.rs
create mode 100644 ui/src/dashboard/tasks.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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v4 1/8] server: task cache: treat a limit of 0 as unbounded
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
@ 2025-08-26 10:15 ` Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 2/8] server: api: remote tasks: add 'remote' filter option Dominik Csapak
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
like we do in our other products e.g. like PBS
This way, one can specify a way to return *all* tasks.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
no changes in v4
server/src/remote_tasks/mod.rs | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/server/src/remote_tasks/mod.rs b/server/src/remote_tasks/mod.rs
index 63a279a..91b79c1 100644
--- a/server/src/remote_tasks/mod.rs
+++ b/server/src/remote_tasks/mod.rs
@@ -36,6 +36,11 @@ pub async fn get_tasks(filters: TaskFilters) -> Result<Vec<TaskListItem>, Error>
GetTasks::All
};
+ let limit = match filters.limit {
+ 0 => usize::MAX,
+ limit => limit as usize,
+ };
+
let returned_tasks = cache
.get_tasks(which)?
.filter_map(|task| {
@@ -105,7 +110,7 @@ pub async fn get_tasks(filters: TaskFilters) -> Result<Vec<TaskListItem>, Error>
true
})
.skip(filters.start as usize)
- .take(filters.limit as usize)
+ .take(limit)
.collect();
Ok(returned_tasks)
--
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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v4 2/8] server: api: remote tasks: add 'remote' filter option
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 1/8] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
@ 2025-08-26 10:15 ` Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 3/8] server: api: add remote-tasks statistics Dominik Csapak
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
so we can filter the tasks by remote
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
no changes in v4
server/src/api/remote_tasks.rs | 16 ++++++++++++----
server/src/remote_tasks/mod.rs | 10 +++++++++-
2 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/server/src/api/remote_tasks.rs b/server/src/api/remote_tasks.rs
index 05ce366..db9fadf 100644
--- a/server/src/api/remote_tasks.rs
+++ b/server/src/api/remote_tasks.rs
@@ -1,5 +1,6 @@
use anyhow::Error;
-use pdm_api_types::{TaskFilters, TaskListItem};
+
+use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, TaskFilters, TaskListItem};
use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
use proxmox_schema::api;
use proxmox_sortable_macro::sortable;
@@ -24,13 +25,20 @@ const SUBDIRS: SubdirMap = &sorted!([("list", &Router::new().get(&API_METHOD_LIS
filters: {
type: TaskFilters,
flatten: true,
- }
+ },
+ remote: {
+ schema: REMOTE_ID_SCHEMA,
+ optional: true,
+ },
},
},
)]
/// Get the list of tasks for all remotes.
-async fn list_tasks(filters: TaskFilters) -> Result<Vec<TaskListItem>, Error> {
- let tasks = remote_tasks::get_tasks(filters).await?;
+async fn list_tasks(
+ filters: TaskFilters,
+ remote: Option<String>,
+) -> Result<Vec<TaskListItem>, Error> {
+ let tasks = remote_tasks::get_tasks(filters, remote).await?;
Ok(tasks)
}
diff --git a/server/src/remote_tasks/mod.rs b/server/src/remote_tasks/mod.rs
index 91b79c1..b3adebe 100644
--- a/server/src/remote_tasks/mod.rs
+++ b/server/src/remote_tasks/mod.rs
@@ -26,7 +26,10 @@ const NUMBER_OF_UNCOMPRESSED_FILES: u32 = 2;
/// Get tasks for all remotes
// FIXME: filter for privileges
-pub async fn get_tasks(filters: TaskFilters) -> Result<Vec<TaskListItem>, Error> {
+pub async fn get_tasks(
+ filters: TaskFilters,
+ remote_filter: Option<String>,
+) -> Result<Vec<TaskListItem>, Error> {
tokio::task::spawn_blocking(move || {
let cache = get_cache()?.read()?;
@@ -44,6 +47,11 @@ pub async fn get_tasks(filters: TaskFilters) -> Result<Vec<TaskListItem>, Error>
let returned_tasks = cache
.get_tasks(which)?
.filter_map(|task| {
+ if let Some(remote_filter) = &remote_filter {
+ if task.upid.remote() != remote_filter {
+ return None;
+ }
+ }
// TODO: Handle PBS tasks
let pve_upid: Result<PveUpid, Error> = task.upid.upid.parse();
match pve_upid {
--
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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v4 3/8] server: api: add remote-tasks statistics
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 1/8] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 2/8] server: api: remote tasks: add 'remote' filter option Dominik Csapak
@ 2025-08-26 10:15 ` Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 4/8] ui: refactor remote upid formatter Dominik Csapak
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
this new api call returns task status counts by remote and by type, so
that the ui can display that without having to count these on the client
side.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
no changes in v4
lib/pdm-api-types/src/lib.rs | 41 +++++++++++++++++++
server/src/api/remote_tasks.rs | 74 +++++++++++++++++++++++++++++++++-
2 files changed, 113 insertions(+), 2 deletions(-)
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index 37da134..c506544 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -1,5 +1,6 @@
//! Basic API types used by most of the PDM code.
+use std::collections::HashMap;
use std::fmt;
use anyhow::{bail, Error};
@@ -280,6 +281,46 @@ pub struct TaskListItem {
pub status: Option<String>,
}
+#[api]
+/// Count of tasks by status
+#[derive(Clone, Serialize, Deserialize, Default, PartialEq)]
+#[serde(rename_all = "lowercase")]
+pub struct TaskCount {
+ /// The number of successful tasks
+ pub ok: u64,
+ /// The number of tasks with warnings
+ pub warning: u64,
+ /// The number of failed tasks
+ pub error: u64,
+ /// The number of tasks with an unknown status
+ pub unknown: u64,
+}
+
+#[api{
+ properties: {
+ "by-type": {
+ type: Object,
+ properties: {},
+ additional_properties: true,
+ },
+ "by-remote": {
+ type: Object,
+ properties: {},
+ additional_properties: true,
+ },
+ },
+}]
+/// Lists the task status counts by type and by remote
+#[derive(Clone, Serialize, Deserialize, PartialEq)]
+#[serde(rename_all = "kebab-case")]
+pub struct TaskStatistics {
+ /// A map of worker-types to status counts
+ pub by_type: HashMap<String, TaskCount>,
+ /// A map of remotes to status counts
+ #[serde(default)]
+ pub by_remote: HashMap<String, TaskCount>,
+}
+
pub const NODE_TASKS_LIST_TASKS_RETURN_TYPE: ReturnType = ReturnType {
optional: false,
schema: &ArraySchema::new("A list of tasks.", &TaskListItem::API_SCHEMA).schema(),
diff --git a/server/src/api/remote_tasks.rs b/server/src/api/remote_tasks.rs
index db9fadf..7b97b9c 100644
--- a/server/src/api/remote_tasks.rs
+++ b/server/src/api/remote_tasks.rs
@@ -1,6 +1,11 @@
+use std::collections::HashMap;
+
use anyhow::Error;
-use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, TaskFilters, TaskListItem};
+use pdm_api_types::{
+ remotes::REMOTE_ID_SCHEMA, RemoteUpid, TaskCount, TaskFilters, TaskListItem, TaskStateType,
+ TaskStatistics,
+};
use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap};
use proxmox_schema::api;
use proxmox_sortable_macro::sortable;
@@ -12,7 +17,13 @@ pub const ROUTER: Router = Router::new()
.subdirs(SUBDIRS);
#[sortable]
-const SUBDIRS: SubdirMap = &sorted!([("list", &Router::new().get(&API_METHOD_LIST_TASKS)),]);
+const SUBDIRS: SubdirMap = &sorted!([
+ ("list", &Router::new().get(&API_METHOD_LIST_TASKS)),
+ (
+ "statistics",
+ &Router::new().get(&API_METHOD_TASK_STATISTICS)
+ ),
+]);
#[api(
// FIXME:: see list-like API calls in resource routers, we probably want more fine-grained
@@ -42,3 +53,62 @@ async fn list_tasks(
Ok(tasks)
}
+
+#[api(
+ // FIXME:: see list-like API calls in resource routers, we probably want more fine-grained
+ // checks..
+ access: {
+ permission: &Permission::Anybody,
+ },
+ input: {
+ properties: {
+ filters: {
+ type: TaskFilters,
+ flatten: true,
+ },
+ remote: {
+ schema: REMOTE_ID_SCHEMA,
+ optional: true,
+ },
+ },
+ },
+)]
+/// Get task statistics for the specified filters.
+async fn task_statistics(
+ filters: TaskFilters,
+ remote: Option<String>,
+) -> Result<TaskStatistics, Error> {
+ let tasks = remote_tasks::get_tasks(filters, remote).await?;
+
+ let mut by_type: HashMap<String, TaskCount> = HashMap::new();
+ let mut by_remote: HashMap<String, TaskCount> = HashMap::new();
+
+ for task in tasks {
+ let status: TaskStateType = match task.status.as_deref() {
+ Some(status) => TaskStateType::new_from_str(status),
+ None => continue,
+ };
+ let entry = by_type.entry(task.worker_type).or_default();
+ match status {
+ TaskStateType::OK => entry.ok += 1,
+ TaskStateType::Warning => entry.warning += 1,
+ TaskStateType::Error => entry.error += 1,
+ TaskStateType::Unknown => entry.unknown += 1,
+ }
+
+ let remote = match task.upid.parse::<RemoteUpid>() {
+ Ok(upid) => upid.remote().to_owned(),
+ Err(_) => continue,
+ };
+
+ let entry = by_remote.entry(remote).or_default();
+ match status {
+ TaskStateType::OK => entry.ok += 1,
+ TaskStateType::Warning => entry.warning += 1,
+ TaskStateType::Error => entry.error += 1,
+ TaskStateType::Unknown => entry.unknown += 1,
+ }
+ }
+
+ Ok(TaskStatistics { by_type, by_remote })
+}
--
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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v4 4/8] ui: refactor remote upid formatter
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
` (2 preceding siblings ...)
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 3/8] server: api: add remote-tasks statistics Dominik Csapak
@ 2025-08-26 10:15 ` Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 5/8] ui: tasks: add helper to summarize task categories Dominik Csapak
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
since that is often the same (aside from the option to render the
remote), and we'll want to reuse that even further.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
---
changes from v3:
* fixed a clippy lint with unnecessary taking a reference on a reference
ui/src/remotes/tasks.rs | 19 ++++---------------
ui/src/tasks.rs | 24 +++++++++++++++++++++++-
ui/src/top_nav_bar.rs | 18 +++---------------
3 files changed, 30 insertions(+), 31 deletions(-)
diff --git a/ui/src/remotes/tasks.rs b/ui/src/remotes/tasks.rs
index 3398647..138b899 100644
--- a/ui/src/remotes/tasks.rs
+++ b/ui/src/remotes/tasks.rs
@@ -7,12 +7,9 @@ use yew::{
};
use pdm_api_types::RemoteUpid;
-use pdm_client::types::PveUpid;
use proxmox_yew_comp::{
- common_api_types::TaskListItem,
- utils::{format_task_description, format_upid, render_epoch_short},
- TaskViewer, Tasks,
+ common_api_types::TaskListItem, utils::render_epoch_short, TaskViewer, Tasks,
};
use pwt::{
css::{FlexFit, JustifyContent},
@@ -24,6 +21,8 @@ use pwt::{
},
};
+use crate::tasks::format_optional_remote_upid;
+
#[derive(PartialEq, Properties)]
pub struct RemoteTaskList;
impl RemoteTaskList {
@@ -77,17 +76,7 @@ fn columns() -> Rc<Vec<DataTableHeader<TaskListItem>>> {
DataTableColumn::new(tr!("Description"))
.flex(4)
.render(move |item: &TaskListItem| {
- if let Ok(remote_upid) = item.upid.parse::<RemoteUpid>() {
- match remote_upid.upid.parse::<PveUpid>() {
- Ok(upid) => {
- format_task_description(&upid.worker_type, upid.worker_id.as_deref())
- }
- Err(_) => format_upid(&remote_upid.upid),
- }
- } else {
- format_upid(&item.upid)
- }
- .into()
+ format_optional_remote_upid(&item.upid, false).into()
})
.into(),
DataTableColumn::new(tr!("Status"))
diff --git a/ui/src/tasks.rs b/ui/src/tasks.rs
index 6aa202a..1129e6a 100644
--- a/ui/src/tasks.rs
+++ b/ui/src/tasks.rs
@@ -1,6 +1,10 @@
-use proxmox_yew_comp::utils::register_task_description;
+use proxmox_yew_comp::utils::{format_task_description, format_upid, register_task_description};
use pwt::tr;
+use pdm_api_types::RemoteUpid;
+use pdm_client::types::PveUpid;
+use yew::virtual_dom::Key;
+
pub fn register_pve_tasks() {
register_task_description("qmstart", ("VM", tr!("Start")));
register_task_description("acmedeactivate", ("ACME Account", tr!("Deactivate")));
@@ -99,3 +103,21 @@ pub fn register_pve_tasks() {
register_task_description("zfscreate", (tr!("ZFS Storage"), tr!("Create")));
register_task_description("zfsremove", ("ZFS Pool", tr!("Remove")));
}
+
+/// Format a UPID that is either [`RemoteUpid`] or a [`UPID`]
+/// If it's a [`RemoteUpid`], prefixes it with the remote name
+pub fn format_optional_remote_upid(upid: &str, include_remote: bool) -> String {
+ if let Ok(remote_upid) = upid.parse::<RemoteUpid>() {
+ let description = match remote_upid.upid.parse::<PveUpid>() {
+ Ok(upid) => format_task_description(&upid.worker_type, upid.worker_id.as_deref()),
+ Err(_) => format_upid(&remote_upid.upid),
+ };
+ if include_remote {
+ format!("{} - {}", remote_upid.remote(), description)
+ } else {
+ description
+ }
+ } else {
+ format_upid(upid)
+ }
+}
diff --git a/ui/src/top_nav_bar.rs b/ui/src/top_nav_bar.rs
index 78aace5..069e831 100644
--- a/ui/src/top_nav_bar.rs
+++ b/ui/src/top_nav_bar.rs
@@ -13,15 +13,15 @@ use pwt::state::{Loader, Theme, ThemeObserver};
use pwt::widget::{Button, Container, Row, ThemeModeSelector, Tooltip};
use proxmox_yew_comp::common_api_types::TaskListItem;
-use proxmox_yew_comp::utils::{format_task_description, format_upid, set_location_href};
+use proxmox_yew_comp::utils::set_location_href;
use proxmox_yew_comp::RunningTasksButton;
use proxmox_yew_comp::{http_get, HelpButton, LanguageDialog, TaskViewer, ThemeDialog};
use pwt_macros::builder;
use pdm_api_types::RemoteUpid;
-use pdm_client::types::PveUpid;
+use crate::tasks::format_optional_remote_upid;
use crate::widget::SearchBox;
#[derive(Deserialize)]
@@ -207,19 +207,7 @@ impl Component for PdmTopNavBar {
}),
])
.render(|item: &TaskListItem| {
- if let Ok(remote_upid) = (&item.upid).parse::<RemoteUpid>() {
- let description = match remote_upid.upid.parse::<PveUpid>() {
- Ok(upid) => format_task_description(
- &upid.worker_type,
- upid.worker_id.as_deref(),
- ),
- Err(_) => format_upid(&remote_upid.upid),
- };
- format!("{} - {}", remote_upid.remote(), description)
- } else {
- format_upid(&item.upid)
- }
- .into()
+ format_optional_remote_upid(&item.upid, true).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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v4 5/8] ui: tasks: add helper to summarize task categories
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
` (3 preceding siblings ...)
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 4/8] ui: refactor remote upid formatter Dominik Csapak
@ 2025-08-26 10:15 ` Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 6/8] ui: add dialog to show filtered tasks Dominik Csapak
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
introduce a helper struct that can easily hold the info we need for
which category a workertype falls in. With methods to create from a str
(intentionally not using FromStr or From<&str> here as those should be
obvious what they do and having an explicit function is better here,
same for having to_filter and to_title, instead of display)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v3:
* use a struct instead of free standing function that convert between
strings
ui/src/tasks.rs | 79 +++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)
diff --git a/ui/src/tasks.rs b/ui/src/tasks.rs
index 1129e6a..5e4a44e 100644
--- a/ui/src/tasks.rs
+++ b/ui/src/tasks.rs
@@ -121,3 +121,82 @@ pub fn format_optional_remote_upid(upid: &str, include_remote: bool) -> String {
format_upid(upid)
}
}
+
+/// Map worker types to sensible categories (that can also be used as filter for the api)
+#[derive(Clone, PartialEq, PartialOrd, Eq, Ord)]
+/// Map worker types to sensible categories
+pub enum TaskWorkerType {
+ Migrate,
+ Qemu,
+ Lxc,
+ Ceph,
+ Ha,
+ Backup,
+ Other(String),
+ Remote(String),
+}
+
+/// Map a category from [`map_worker_type`] to a title text.
+impl TaskWorkerType {
+ /// Create new from a given worker type string, e.g. from a UPID
+ pub fn new_from_str<A: AsRef<str>>(worker_type: A) -> Self {
+ match worker_type.as_ref() {
+ task_type if task_type.contains("migrate") => TaskWorkerType::Migrate,
+ task_type if task_type.starts_with("qm") => TaskWorkerType::Qemu,
+ task_type if task_type.starts_with("vz") && task_type != "vzdump" => {
+ TaskWorkerType::Lxc
+ }
+ task_type if task_type == "vzdump" => TaskWorkerType::Backup,
+ task_type if task_type.starts_with("ceph") => TaskWorkerType::Ceph,
+ task_type if task_type.starts_with("ha") => TaskWorkerType::Ha,
+ other => TaskWorkerType::Other(other.to_string()),
+ }
+ }
+
+ /// Create title from the categories
+ pub fn to_title(&self) -> String {
+ match self {
+ TaskWorkerType::Migrate => tr!("Guest Migrations"),
+ TaskWorkerType::Qemu => tr!("Virtual Machine related Tasks"),
+ TaskWorkerType::Lxc => tr!("Container related Tasks"),
+ TaskWorkerType::Ceph => tr!("Ceph related Tasks"),
+ TaskWorkerType::Ha => tr!("HA related Tasks"),
+ TaskWorkerType::Backup => tr!("Backups and Backup Jobs"),
+ TaskWorkerType::Other(other) => other.to_string(),
+ TaskWorkerType::Remote(remote) => remote.to_string(),
+ }
+ }
+
+ /// Create filter string for the api
+ ///
+ /// Note: The result has to be filtered with this again, since more records will be returned.
+ /// E.g. using 'vz' will also return 'vzdump' tasks which are not desired.
+ pub fn to_filter<'a>(&'a self) -> &'a str {
+ match self {
+ TaskWorkerType::Migrate => "migrate",
+ TaskWorkerType::Qemu => "qm",
+ TaskWorkerType::Lxc => "vz",
+ TaskWorkerType::Ceph => "ceph",
+ TaskWorkerType::Ha => "ha",
+ TaskWorkerType::Backup => "vzdump",
+ TaskWorkerType::Other(other) => other.as_str(),
+ TaskWorkerType::Remote(remote) => remote.as_str(),
+ }
+ }
+}
+
+impl From<TaskWorkerType> for Key {
+ fn from(value: TaskWorkerType) -> Self {
+ match value {
+ TaskWorkerType::Migrate => Key::from("migrate"),
+ TaskWorkerType::Qemu => Key::from("qm"),
+ TaskWorkerType::Lxc => Key::from("vz"),
+ TaskWorkerType::Ceph => Key::from("ceph"),
+ TaskWorkerType::Ha => Key::from("ha"),
+ TaskWorkerType::Backup => Key::from("vzdump"),
+ // use `__` prefix here so that they can't clash with the statically defined ones
+ TaskWorkerType::Other(other) => Key::from(format!("__{other}")),
+ TaskWorkerType::Remote(remote) => Key::from(format!("__remote_{remote}")),
+ }
+ }
+}
--
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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v4 6/8] ui: add dialog to show filtered tasks
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
` (4 preceding siblings ...)
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 5/8] ui: tasks: add helper to summarize task categories Dominik Csapak
@ 2025-08-26 10:15 ` Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 7/8] ui: dashboard: add task summaries Dominik Csapak
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
This is a dialog that gets and shows a list of filtered tasks, filtered
either by UPID worker types or remotes and always a state (so
success,warning or error)
This needs a bit of adaption for the serializer of TaskFilters, so
we can use it to generate the parameters.
Not used yet here, but we'll use it in the dashboard task summary
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v3:
* use TaskWorkerType instead of helpers
* remove TaskGroup helper struct
lib/pdm-api-types/src/lib.rs | 5 +
ui/src/dashboard/filtered_tasks.rs | 291 +++++++++++++++++++++++++++++
ui/src/dashboard/mod.rs | 2 +
3 files changed, 298 insertions(+)
create mode 100644 ui/src/dashboard/filtered_tasks.rs
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index c506544..e31a904 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -549,9 +549,14 @@ pub struct TaskFilters {
pub errors: bool,
#[serde(default)]
pub running: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub userfilter: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub since: Option<i64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub until: Option<i64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub typefilter: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub statusfilter: Option<Vec<TaskStateType>>,
}
diff --git a/ui/src/dashboard/filtered_tasks.rs b/ui/src/dashboard/filtered_tasks.rs
new file mode 100644
index 0000000..6f211c2
--- /dev/null
+++ b/ui/src/dashboard/filtered_tasks.rs
@@ -0,0 +1,291 @@
+use std::rc::Rc;
+
+use anyhow::Error;
+use proxmox_yew_comp::{
+ common_api_types::{TaskListItem, TaskStatusClass},
+ http_get,
+ utils::{format_duration_human, render_epoch},
+ Status, TaskViewer,
+};
+use pwt_macros::builder;
+use yew::{
+ html::IntoEventCallback,
+ virtual_dom::{VComp, VNode},
+ Component, Properties,
+};
+
+use pwt::{
+ css::FlexFit,
+ prelude::*,
+ widget::{
+ data_table::{DataTable, DataTableColumn, DataTableHeader},
+ ActionIcon, AlertDialog, Fa, Mask, Tooltip,
+ },
+ AsyncPool,
+};
+use pwt::{state::Store, tr, widget::Dialog};
+
+use pdm_api_types::{RemoteUpid, TaskFilters, TaskStateType};
+
+use crate::tasks::{format_optional_remote_upid, TaskWorkerType};
+
+#[derive(PartialEq, Properties)]
+#[builder]
+pub struct FilteredTasks {
+ grouping: TaskWorkerType,
+ task_status: TaskStatusClass,
+ since: i64,
+
+ #[prop_or_default]
+ #[builder_cb(IntoEventCallback, into_event_callback, ())]
+ /// Callback for closing the Dialog
+ on_close: Option<Callback<()>>,
+}
+
+impl FilteredTasks {
+ /// Create new instance with filters for task type and status, beginning from 'since'
+ pub fn new(since: i64, grouping: TaskWorkerType, task_status: TaskStatusClass) -> Self {
+ yew::props!(Self {
+ since,
+ grouping,
+ task_status,
+ })
+ }
+}
+
+impl From<FilteredTasks> for VNode {
+ fn from(val: FilteredTasks) -> Self {
+ let comp = VComp::new::<PdmFilteredTasks>(Rc::new(val), None);
+ VNode::from(comp)
+ }
+}
+
+pub enum Msg {
+ LoadFinished(Result<Vec<TaskListItem>, Error>),
+ ShowTask(Option<(RemoteUpid, Option<i64>)>),
+}
+
+pub struct PdmFilteredTasks {
+ task_store: Store<TaskListItem>,
+ task_info: Option<(RemoteUpid, Option<i64>)>,
+ loading: bool,
+ last_error: Option<Error>,
+ _async_pool: AsyncPool,
+}
+
+impl PdmFilteredTasks {
+ async fn load(
+ since: i64,
+ status: TaskStatusClass,
+ grouping: TaskWorkerType,
+ ) -> Result<Vec<TaskListItem>, Error> {
+ // TODO replace with pdm client call
+ let status = match status {
+ TaskStatusClass::Ok => TaskStateType::OK,
+ TaskStatusClass::Warning => TaskStateType::Warning,
+ TaskStatusClass::Error => TaskStateType::Error,
+ };
+ let mut filters = TaskFilters {
+ since: Some(since),
+ limit: 0,
+ userfilter: None,
+ until: None,
+ typefilter: None,
+ statusfilter: Some(vec![status.clone()]),
+
+ start: 0,
+ errors: false,
+ running: false,
+ };
+
+ match &grouping {
+ TaskWorkerType::Remote(_) => {}
+ worker_type => {
+ filters.typefilter = Some(worker_type.to_filter().to_string());
+ }
+ }
+
+ let mut params = serde_json::to_value(filters)?;
+
+ if let TaskWorkerType::Remote(remote) = grouping {
+ params["remote"] = serde_json::Value::String(remote);
+ }
+
+ http_get("/remote-tasks/list", Some(params)).await
+ }
+}
+
+impl Component for PdmFilteredTasks {
+ type Message = Msg;
+ type Properties = FilteredTasks;
+
+ fn create(ctx: &Context<Self>) -> Self {
+ let props = ctx.props();
+ let since = props.since;
+ let grouping = props.grouping.clone();
+ let status = props.task_status;
+ let link = ctx.link().clone();
+ let _async_pool = AsyncPool::new();
+ _async_pool.send_future(link, async move {
+ let res = Self::load(since, status, grouping).await;
+ Msg::LoadFinished(res)
+ });
+ Self {
+ task_store: Store::new(),
+ task_info: None,
+ loading: true,
+ last_error: None,
+ _async_pool,
+ }
+ }
+
+ fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::LoadFinished(Ok(task_list_items)) => {
+ self.last_error = None;
+ self.loading = false;
+ self.task_store.set_data(task_list_items);
+ match _ctx.props().grouping.clone() {
+ TaskWorkerType::Remote(_) => {}
+ worker_type => {
+ self.task_store.set_filter(move |entry: &TaskListItem| {
+ worker_type == TaskWorkerType::new_from_str(&entry.worker_type)
+ });
+ }
+ }
+ }
+ Msg::LoadFinished(Err(err)) => {
+ self.loading = false;
+ self.last_error = Some(err);
+ }
+ Msg::ShowTask(task) => {
+ self.task_info = task;
+ }
+ }
+ true
+ }
+
+ fn view(&self, ctx: &Context<Self>) -> Html {
+ let props = ctx.props();
+ if let Some(err) = &self.last_error {
+ return AlertDialog::new(err.to_string())
+ .on_close(ctx.props().on_close.clone())
+ .into();
+ }
+
+ if let Some((upid, endtime)) = &self.task_info {
+ // TODO PBS
+ let base_url = format!("/pve/remotes/{}/tasks", upid.remote());
+ TaskViewer::new(upid.to_string())
+ .endtime(endtime)
+ .base_url(base_url)
+ .on_close({
+ let link = ctx.link().clone();
+ move |_| link.send_message(Msg::ShowTask(None))
+ })
+ .into()
+ } else {
+ let title = format!(
+ "{} - {}",
+ props.grouping.to_title(),
+ match props.task_status {
+ TaskStatusClass::Ok => tr!("OK"),
+ TaskStatusClass::Warning => tr!("Warning"),
+ TaskStatusClass::Error => tr!("Error"),
+ },
+ );
+ Dialog::new(title)
+ .key(format!("filtered-tasks-{}", self.loading)) // recenters when loading
+ .min_width(800)
+ .min_height(600)
+ .max_height("90vh") // max 90% of the screen height
+ .resizable(true)
+ .on_close(props.on_close.clone())
+ .with_child(
+ Mask::new(
+ DataTable::new(filtered_tasks_columns(ctx), self.task_store.clone())
+ .class(FlexFit),
+ )
+ .class(FlexFit)
+ .visible(self.loading),
+ )
+ .into()
+ }
+ }
+}
+
+fn filtered_tasks_columns(
+ ctx: &Context<PdmFilteredTasks>,
+) -> Rc<Vec<DataTableHeader<TaskListItem>>> {
+ Rc::new(vec![
+ DataTableColumn::new(tr!("Remote"))
+ .width("minmax(150px, 1fr)")
+ .get_property_owned(
+ |item: &TaskListItem| match item.upid.parse::<RemoteUpid>() {
+ Ok(upid) => upid.remote().to_string(),
+ Err(_) => String::new(),
+ },
+ )
+ .into(),
+ DataTableColumn::new(tr!("Task"))
+ .flex(2)
+ .get_property_owned(|item: &TaskListItem| {
+ format_optional_remote_upid(&item.upid, false)
+ })
+ .into(),
+ DataTableColumn::new(tr!("Start Time"))
+ .sort_order(false)
+ .width("200px")
+ .get_property_owned(|item: &TaskListItem| render_epoch(item.starttime))
+ .into(),
+ DataTableColumn::new(tr!("Duration"))
+ .sorter(|a: &TaskListItem, b: &TaskListItem| {
+ let duration_a = match a.endtime {
+ Some(endtime) => endtime - a.starttime,
+ None => i64::MAX,
+ };
+ let duration_b = match b.endtime {
+ Some(endtime) => endtime - b.starttime,
+ None => i64::MAX,
+ };
+ duration_a.cmp(&duration_b)
+ })
+ .render(|item: &TaskListItem| {
+ let duration = match item.endtime {
+ Some(endtime) => endtime - item.starttime,
+ None => return String::from("-").into(),
+ };
+ format_duration_human(duration as f64).into()
+ })
+ .into(),
+ DataTableColumn::new(tr!("Status"))
+ .justify("center")
+ .render(|item: &TaskListItem| {
+ let text = item.status.as_deref().unwrap_or("");
+ let icon = match text.into() {
+ TaskStatusClass::Ok => Status::Success,
+ TaskStatusClass::Warning => Status::Warning,
+ TaskStatusClass::Error => Status::Error,
+ };
+ Fa::from(icon).into()
+ })
+ .into(),
+ DataTableColumn::new(tr!("Action"))
+ .justify("center")
+ .render({
+ let link = ctx.link().clone();
+ move |item: &TaskListItem| {
+ let upid = item.upid.clone();
+ let endtime = item.endtime;
+ let link = link.clone();
+ let icon = ActionIcon::new("fa fa-chevron-right").on_activate(move |_| {
+ if let Ok(upid) = upid.parse::<RemoteUpid>() {
+ link.send_message(Msg::ShowTask(Some((upid, endtime))));
+ }
+ });
+ Tooltip::new(icon).tip(tr!("Open Task")).into()
+ }
+ })
+ .into(),
+ ])
+}
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 84efb1b..0cdd26c 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -45,6 +45,8 @@ use guest_panel::GuestPanel;
mod status_row;
use status_row::DashboardStatusRow;
+mod filtered_tasks;
+
/// The default 'max-age' parameter in seconds.
pub const DEFAULT_MAX_AGE_S: u64 = 60;
--
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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v4 7/8] ui: dashboard: add task summaries
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
` (5 preceding siblings ...)
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 6/8] ui: add dialog to show filtered tasks Dominik Csapak
@ 2025-08-26 10:15 ` Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 8/8] ui: dashboard: make task summary time range configurable Dominik Csapak
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
similar to what we show in PBS.
We add two panels:
* one for summarizing by category (e.g. guest migration)
* one by the 5 remotes with the most tasks
Each shows a list of categories/remotes with a count per state
(success,warning,error). A click on such a count opens a filtered
view that shows only those tasks.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v3:
* use BTreeMap instead of Hashmap for sorting
* use TaskWorkerType instead of helper functions
ui/src/dashboard/mod.rs | 108 +++++++++++++-
ui/src/dashboard/tasks.rs | 301 ++++++++++++++++++++++++++++++++++++++
2 files changed, 403 insertions(+), 6 deletions(-)
create mode 100644 ui/src/dashboard/tasks.rs
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 0cdd26c..502332a 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -1,7 +1,7 @@
use std::{collections::HashMap, rc::Rc};
use anyhow::Error;
-use futures::future::join;
+use futures::join;
use js_sys::Date;
use serde::{Deserialize, Serialize};
use serde_json::json;
@@ -24,7 +24,10 @@ use pwt::{
AsyncPool,
};
-use pdm_api_types::resource::{GuestStatusCount, NodeStatusCount, ResourcesStatus};
+use pdm_api_types::{
+ resource::{GuestStatusCount, NodeStatusCount, ResourcesStatus},
+ TaskStatistics,
+};
use pdm_client::types::TopEntity;
use proxmox_client::ApiResponseData;
@@ -47,6 +50,9 @@ use status_row::DashboardStatusRow;
mod filtered_tasks;
+mod tasks;
+use tasks::TaskSummary;
+
/// The default 'max-age' parameter in seconds.
pub const DEFAULT_MAX_AGE_S: u64 = 60;
@@ -80,6 +86,7 @@ pub struct DashboardConfig {
pub type LoadingResult = (
Result<ResourcesStatus, Error>,
Result<pdm_client::types::TopEntities, proxmox_client::Error>,
+ Result<TaskStatistics, Error>,
);
pub enum Msg {
@@ -91,11 +98,19 @@ pub enum Msg {
ConfigWindow(bool),
}
+struct StatisticsOptions {
+ hours: u32,
+ since: i64,
+ data: Option<TaskStatistics>,
+ error: Option<Error>,
+}
+
pub struct PdmDashboard {
status: ResourcesStatus,
last_error: Option<Error>,
top_entities: Option<pdm_client::types::TopEntities>,
last_top_entities_error: Option<proxmox_client::Error>,
+ statistics: StatisticsOptions,
loading: bool,
load_finished_time: Option<f64>,
remote_list: RemoteList,
@@ -174,6 +189,47 @@ impl PdmDashboard {
))
}
+ fn create_task_summary_panel(
+ &self,
+ statistics: &StatisticsOptions,
+ remotes: Option<u32>,
+ ) -> Panel {
+ let title = match remotes {
+ Some(count) => tr!(
+ "Task Summary for Top {0} Remotes (Last {1}h)",
+ count,
+ statistics.hours
+ ),
+ None => tr!("Task Summary by Category (Last {0}h)", statistics.hours),
+ };
+ Panel::new()
+ .flex(1.0)
+ .width(500)
+ .border(true)
+ .title(self.create_title_with_icon("list", title))
+ .with_child(
+ Container::new()
+ .class(FlexFit)
+ .padding(2)
+ .with_optional_child(
+ statistics
+ .data
+ .clone()
+ .map(|data| TaskSummary::new(data, statistics.since, remotes)),
+ )
+ .with_optional_child(
+ (statistics.error.is_none() && statistics.data.is_none())
+ .then_some(loading_column()),
+ )
+ .with_optional_child(
+ statistics
+ .error
+ .as_ref()
+ .map(|err| error_message(&err.to_string())),
+ ),
+ )
+ }
+
fn create_top_entities_panel(
&self,
icon: &str,
@@ -209,9 +265,23 @@ impl PdmDashboard {
let top_entities_future = client.get_top_entities();
let status_future = http_get("/resources/status", Some(json!({"max-age": max_age})));
- let (top_entities_res, status_res) = join(top_entities_future, status_future).await;
+ let since = (Date::now() / 1000.0) as i64 - (24 * 60 * 60);
+ let params = Some(json!({
+ "since": since,
+ "limit": 0,
+ }));
+
+ // TODO replace with pdm client call
+ let statistics_future = http_get("/remote-tasks/statistics", params);
- link.send_message(Msg::LoadingFinished((status_res, top_entities_res)));
+ let (top_entities_res, status_res, statistics_res) =
+ join!(top_entities_future, status_future, statistics_future);
+
+ link.send_message(Msg::LoadingFinished((
+ status_res,
+ top_entities_res,
+ statistics_res,
+ )));
});
}
}
@@ -229,11 +299,20 @@ impl Component for PdmDashboard {
.context(ctx.link().callback(Msg::RemoteListChanged))
.expect("No Remote list context provided");
+ let since = (Date::now() / 1000.0) as i64 - (24 * 60 * 60);
+ let statistics = StatisticsOptions {
+ hours: 24,
+ since,
+ data: None,
+ error: None,
+ };
+
let mut this = Self {
status: ResourcesStatus::default(),
last_error: None,
top_entities: None,
last_top_entities_error: None,
+ statistics,
loading: true,
load_finished_time: None,
remote_list,
@@ -251,7 +330,7 @@ impl Component for PdmDashboard {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
- Msg::LoadingFinished((resources_status, top_entities)) => {
+ Msg::LoadingFinished((resources_status, top_entities, task_statistics)) => {
match resources_status {
Ok(status) => {
self.last_error = None;
@@ -266,6 +345,13 @@ impl Component for PdmDashboard {
}
Err(err) => self.last_top_entities_error = Some(err),
}
+ match task_statistics {
+ Ok(statistics) => {
+ self.statistics.error = None;
+ self.statistics.data = Some(statistics);
+ }
+ Err(err) => self.statistics.error = Some(err),
+ }
self.loading = false;
self.load_finished_time = Some(Date::now() / 1000.0);
true
@@ -412,7 +498,6 @@ impl Component for PdmDashboard {
.class("pwt-content-spacer")
.class(FlexDirection::Row)
.class("pwt-align-content-start")
- .class(pwt::css::Flex::Fill)
.padding_top(0)
.class(FlexWrap::Wrap)
//.min_height(175)
@@ -434,6 +519,17 @@ impl Component for PdmDashboard {
tr!("Memory usage"),
self.top_entities.as_ref().map(|e| &e.node_memory),
)),
+ )
+ .with_child(
+ Container::new()
+ .class("pwt-content-spacer")
+ .class(FlexDirection::Row)
+ .class("pwt-align-content-start")
+ .style("padding-top", "0")
+ .class(pwt::css::Flex::Fill)
+ .class(FlexWrap::Wrap)
+ .with_child(self.create_task_summary_panel(&self.statistics, None))
+ .with_child(self.create_task_summary_panel(&self.statistics, Some(5))),
);
Panel::new()
diff --git a/ui/src/dashboard/tasks.rs b/ui/src/dashboard/tasks.rs
new file mode 100644
index 0000000..fc38aea
--- /dev/null
+++ b/ui/src/dashboard/tasks.rs
@@ -0,0 +1,301 @@
+use std::collections::BTreeMap;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use yew::html::Scope;
+use yew::virtual_dom::Key;
+
+use proxmox_yew_comp::common_api_types::TaskStatusClass;
+use pwt::css;
+use pwt::prelude::*;
+use pwt::props::ExtractPrimaryKey;
+use pwt::state::Store;
+use pwt::widget::data_table::{DataTable, DataTableColumn, DataTableHeader};
+use pwt::widget::{ActionIcon, Container, Tooltip};
+use pwt_macros::{builder, widget};
+
+use pdm_api_types::TaskStatistics;
+
+use crate::tasks::TaskWorkerType;
+
+use super::filtered_tasks::FilteredTasks;
+
+#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
+struct TaskSummaryItem {
+ group: TaskWorkerType,
+ error_count: u64,
+ warning_count: u64,
+ ok_count: u64,
+ unknown_count: u64,
+}
+
+impl TaskSummaryItem {
+ fn new(group: TaskWorkerType) -> Self {
+ TaskSummaryItem {
+ group,
+ error_count: 0,
+ warning_count: 0,
+ ok_count: 0,
+ unknown_count: 0,
+ }
+ }
+}
+
+impl ExtractPrimaryKey for TaskSummaryItem {
+ fn extract_key(&self) -> Key {
+ Key::from(self.group.clone())
+ }
+}
+
+#[widget(comp=ProxmoxTaskSummary, @element)]
+#[derive(Clone, PartialEq, Properties)]
+#[builder]
+pub struct TaskSummary {
+ statistics: TaskStatistics,
+
+ since: i64,
+
+ top_remotes: Option<u32>,
+}
+
+impl TaskSummary {
+ /// New Task Summary, if `top_remotes` is `Some`, group
+ /// by that much remotes instead of predefined groups.
+ pub fn new(statistics: TaskStatistics, since: i64, top_remotes: Option<u32>) -> Self {
+ yew::props!(Self {
+ statistics,
+ since,
+ top_remotes
+ })
+ }
+}
+
+pub enum Msg {
+ ShowFilteredTasks(Option<(TaskWorkerType, TaskStatusClass)>), // task_tyope
+}
+
+#[derive(PartialEq)]
+pub enum ViewState {}
+
+#[doc(hidden)]
+pub struct ProxmoxTaskSummary {
+ store: Store<TaskSummaryItem>,
+ task_filters: Option<(TaskWorkerType, TaskStatusClass)>,
+}
+
+fn extract_task_summary(data: &TaskStatistics) -> Vec<TaskSummaryItem> {
+ let mut map: BTreeMap<TaskWorkerType, TaskSummaryItem> = BTreeMap::new();
+
+ let mut insert_type = |task_type: TaskWorkerType| {
+ map.insert(task_type.clone(), TaskSummaryItem::new(task_type));
+ };
+
+ insert_type(TaskWorkerType::Migrate);
+ insert_type(TaskWorkerType::Qemu);
+ insert_type(TaskWorkerType::Lxc);
+ insert_type(TaskWorkerType::Ceph);
+ insert_type(TaskWorkerType::Backup);
+ insert_type(TaskWorkerType::Ha);
+
+ for (worker_type, count) in data.by_type.iter() {
+ let task_type = TaskWorkerType::new_from_str(&worker_type);
+
+ let entry = match map.get_mut(&task_type) {
+ Some(entry) => entry,
+ None => continue,
+ };
+
+ entry.ok_count += count.ok;
+ entry.warning_count += count.warning;
+ entry.error_count += count.error;
+ entry.unknown_count += count.unknown;
+ }
+
+ map.into_values().collect()
+}
+
+fn extract_task_summary_remote(data: &TaskStatistics, limit: u32) -> Vec<TaskSummaryItem> {
+ let mut map: HashMap<String, TaskSummaryItem> = HashMap::new();
+
+ for (remote, count) in data.by_remote.iter() {
+ let entry = map
+ .entry(remote.to_string())
+ .or_insert_with(|| TaskSummaryItem::new(TaskWorkerType::Remote(remote.to_string())));
+
+ entry.ok_count += count.ok;
+ entry.warning_count += count.warning;
+ entry.error_count += count.error;
+ entry.unknown_count += count.unknown;
+ }
+
+ let mut list: Vec<TaskSummaryItem> = map.into_values().collect();
+ list.sort_by(|a, b| {
+ let a_count = a.error_count + a.warning_count + a.ok_count;
+ let b_count = b.error_count + b.warning_count + b.ok_count;
+ b_count.cmp(&a_count)
+ });
+
+ list.into_iter().take(limit as usize).collect()
+}
+
+fn render_counter(
+ link: Scope<ProxmoxTaskSummary>,
+ count: u64,
+ task_type: TaskWorkerType,
+ task_class: TaskStatusClass,
+) -> Html {
+ let (icon_class, icon_scheme, state_text) = match task_class {
+ TaskStatusClass::Ok => ("fa-check", css::ColorScheme::Success, tr!("OK")),
+ TaskStatusClass::Warning => (
+ "fa-exclamation-triangle",
+ css::ColorScheme::Warning,
+ tr!("Warning"),
+ ),
+ TaskStatusClass::Error => ("fa-times-circle", css::ColorScheme::Error, tr!("Error")),
+ };
+ let tip = tr!("Show: {0} - {1}", task_type.to_title(), state_text);
+
+ let has_count = count > 0;
+ let action = ActionIcon::new(classes!("fa", icon_class))
+ .margin_end(1)
+ .class(has_count.then_some(icon_scheme))
+ .disabled(!has_count)
+ .on_activate(move |_| {
+ link.send_message(Msg::ShowFilteredTasks(Some((
+ task_type.clone(),
+ task_class,
+ ))))
+ });
+
+ Tooltip::new(
+ Container::from_tag("span")
+ .with_child(action)
+ .with_child(count),
+ )
+ .tip(has_count.then_some(tip))
+ .into()
+}
+
+impl ProxmoxTaskSummary {
+ fn task_summary_columns(
+ &self,
+ ctx: &Context<Self>,
+ ) -> Rc<Vec<DataTableHeader<TaskSummaryItem>>> {
+ Rc::new(vec![
+ DataTableColumn::new("")
+ .flex(1)
+ .get_property_owned(|item: &TaskSummaryItem| item.group.to_title())
+ .into(),
+ DataTableColumn::new("")
+ .width("100px")
+ .render({
+ let link = ctx.link().clone();
+ move |item: &TaskSummaryItem| {
+ render_counter(
+ link.clone(),
+ item.error_count,
+ item.group.clone(),
+ TaskStatusClass::Error,
+ )
+ }
+ })
+ .into(),
+ DataTableColumn::new("")
+ .width("100px")
+ .render({
+ let link = ctx.link().clone();
+ move |item: &TaskSummaryItem| {
+ render_counter(
+ link.clone(),
+ item.warning_count,
+ item.group.clone(),
+ TaskStatusClass::Warning,
+ )
+ }
+ })
+ .into(),
+ DataTableColumn::new("")
+ .width("100px")
+ .render({
+ let link = ctx.link().clone();
+ move |item: &TaskSummaryItem| {
+ render_counter(
+ link.clone(),
+ item.ok_count,
+ item.group.clone(),
+ TaskStatusClass::Ok,
+ )
+ }
+ })
+ .into(),
+ ])
+ }
+
+ fn update_task_statistics(&mut self, ctx: &Context<Self>) {
+ let store = &self.store;
+
+ if let Some(top_remotes) = ctx.props().top_remotes {
+ store.set_data(extract_task_summary_remote(
+ &ctx.props().statistics,
+ top_remotes,
+ ));
+ } else {
+ store.set_data(extract_task_summary(&ctx.props().statistics));
+ }
+ }
+}
+
+impl Component for ProxmoxTaskSummary {
+ type Properties = TaskSummary;
+ type Message = Msg;
+
+ fn create(ctx: &Context<Self>) -> Self {
+ let mut this = Self {
+ store: Store::new(),
+ task_filters: None,
+ };
+
+ this.update_task_statistics(ctx);
+ this
+ }
+
+ fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::ShowFilteredTasks(filters) => {
+ self.task_filters = filters;
+ }
+ }
+ true
+ }
+
+ fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
+ if old_props.statistics != ctx.props().statistics {
+ self.update_task_statistics(ctx);
+ }
+
+ true
+ }
+
+ fn view(&self, ctx: &Context<Self>) -> Html {
+ let props = ctx.props();
+ let tasks = self.task_filters.as_ref().map(|(task_type, task_status)| {
+ FilteredTasks::new(props.since, task_type.clone(), *task_status).on_close({
+ let link = ctx.link().clone();
+ move |_| link.send_message(Msg::ShowFilteredTasks(None))
+ })
+ });
+
+ Container::new()
+ .class(css::FlexFit)
+ .with_child(
+ DataTable::new(self.task_summary_columns(ctx), self.store.clone())
+ .class(pwt::css::FlexFit)
+ .striped(false)
+ .borderless(true)
+ .hover(true)
+ .show_header(false),
+ )
+ .with_optional_child(tasks)
+ .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] 11+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v4 8/8] ui: dashboard: make task summary time range configurable
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
` (6 preceding siblings ...)
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 7/8] ui: dashboard: add task summaries Dominik Csapak
@ 2025-08-26 10:15 ` Dominik Csapak
2025-08-26 12:41 ` [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Stefan Hanreich
2025-09-04 19:21 ` [pdm-devel] applied-series: " Thomas Lamprecht
9 siblings, 0 replies; 11+ messages in thread
From: Dominik Csapak @ 2025-08-26 10:15 UTC (permalink / raw)
To: pdm-devel
in the browsers local storage.
We already have now an interface to configure this, so use it to store
the timeframe to check for tasks
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v3:
* deduplicate hours/since calculation
* remove hours/since from the StatisticsOptions since it's saved in the
dashboard_config
ui/src/dashboard/mod.rs | 56 ++++++++++++++++++++++++++---------------
1 file changed, 36 insertions(+), 20 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 502332a..cc62870 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -59,6 +59,9 @@ pub const DEFAULT_MAX_AGE_S: u64 = 60;
/// The default refresh interval
pub const DEFAULT_REFRESH_INTERVAL_S: u64 = DEFAULT_MAX_AGE_S / 2;
+/// The default hours to show for task summaries
+pub const DEFAULT_TASK_SUMMARY_HOURS: u32 = 24;
+
#[derive(Properties, PartialEq)]
pub struct Dashboard {}
@@ -81,6 +84,8 @@ pub struct DashboardConfig {
refresh_interval: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
max_age: Option<u64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ task_last_hours: Option<u32>,
}
pub type LoadingResult = (
@@ -99,8 +104,6 @@ pub enum Msg {
}
struct StatisticsOptions {
- hours: u32,
- since: i64,
data: Option<TaskStatistics>,
error: Option<Error>,
}
@@ -194,13 +197,10 @@ impl PdmDashboard {
statistics: &StatisticsOptions,
remotes: Option<u32>,
) -> Panel {
+ let (hours, since) = Self::get_task_options(&self.config);
let title = match remotes {
- Some(count) => tr!(
- "Task Summary for Top {0} Remotes (Last {1}h)",
- count,
- statistics.hours
- ),
- None => tr!("Task Summary by Category (Last {0}h)", statistics.hours),
+ Some(count) => tr!("Task Summary for Top {0} Remotes (Last {1}h)", count, hours),
+ None => tr!("Task Summary by Category (Last {0}h)", hours),
};
Panel::new()
.flex(1.0)
@@ -215,7 +215,7 @@ impl PdmDashboard {
statistics
.data
.clone()
- .map(|data| TaskSummary::new(data, statistics.since, remotes)),
+ .map(|data| TaskSummary::new(data, since, remotes)),
)
.with_optional_child(
(statistics.error.is_none() && statistics.data.is_none())
@@ -257,6 +257,7 @@ impl PdmDashboard {
fn reload(&mut self, ctx: &yew::Context<Self>) {
let link = ctx.link().clone();
let max_age = self.config.max_age.unwrap_or(DEFAULT_MAX_AGE_S);
+ let (_, since) = Self::get_task_options(&self.config);
self.load_finished_time = None;
self.async_pool.spawn(async move {
@@ -265,7 +266,6 @@ impl PdmDashboard {
let top_entities_future = client.get_top_entities();
let status_future = http_get("/resources/status", Some(json!({"max-age": max_age})));
- let since = (Date::now() / 1000.0) as i64 - (24 * 60 * 60);
let params = Some(json!({
"since": since,
"limit": 0,
@@ -284,6 +284,12 @@ impl PdmDashboard {
)));
});
}
+
+ fn get_task_options(config: &PersistentState<DashboardConfig>) -> (u32, i64) {
+ let hours = config.task_last_hours.unwrap_or(DEFAULT_TASK_SUMMARY_HOURS);
+ let since = (Date::now() / 1000.0) as i64 - (hours * 60 * 60) as i64;
+ (hours, since)
+ }
}
impl Component for PdmDashboard {
@@ -291,7 +297,8 @@ impl Component for PdmDashboard {
type Properties = Dashboard;
fn create(ctx: &yew::Context<Self>) -> Self {
- let config = PersistentState::new(StorageLocation::local("dashboard-config"));
+ let config: PersistentState<DashboardConfig> =
+ PersistentState::new(StorageLocation::local("dashboard-config"));
let async_pool = AsyncPool::new();
let (remote_list, _context_listener) = ctx
@@ -299,20 +306,15 @@ impl Component for PdmDashboard {
.context(ctx.link().callback(Msg::RemoteListChanged))
.expect("No Remote list context provided");
- let since = (Date::now() / 1000.0) as i64 - (24 * 60 * 60);
- let statistics = StatisticsOptions {
- hours: 24,
- since,
- data: None,
- error: None,
- };
-
let mut this = Self {
status: ResourcesStatus::default(),
last_error: None,
top_entities: None,
last_top_entities_error: None,
- statistics,
+ statistics: StatisticsOptions {
+ data: None,
+ error: None,
+ },
loading: true,
load_finished_time: None,
remote_list,
@@ -374,7 +376,14 @@ impl Component for PdmDashboard {
true
}
Msg::UpdateConfig(dashboard_config) => {
+ let (old_hours, _) = Self::get_task_options(&self.config);
self.config.update(dashboard_config);
+ let (new_hours, _) = Self::get_task_options(&self.config);
+
+ if old_hours != new_hours {
+ self.reload(ctx);
+ }
+
self.show_config_window = false;
true
}
@@ -592,6 +601,13 @@ impl Component for PdmDashboard {
DisplayField::new()
.key("max-age-explanation")
.value(tr!("If a response from a remote is older than 'Max Age', it will be updated on the next refresh.")))
+ .with_field(
+ tr!("Task Summary Time Range (last hours)"),
+ Number::new()
+ .name("task-last-hours")
+ .min(0u64)
+ .placeholder(DEFAULT_TASK_SUMMARY_HOURS.to_string()),
+ )
.into()
})
.on_close(ctx.link().callback(|_| Msg::ConfigWindow(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] 11+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
` (7 preceding siblings ...)
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 8/8] ui: dashboard: make task summary time range configurable Dominik Csapak
@ 2025-08-26 12:41 ` Stefan Hanreich
2025-09-04 19:21 ` [pdm-devel] applied-series: " Thomas Lamprecht
9 siblings, 0 replies; 11+ messages in thread
From: Stefan Hanreich @ 2025-08-26 12:41 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
Looked at the changes - series LGTM from my POV now.
I also gave this another quick spin on my PDM instance:
Ran some tasks and re-checked the Dashboard overview if the information
has been correctly updated / applied. Did cover each remote / task type
/ result type (ok, warning, ..) once with the tasks I ran.
Played with the different duration settings as well.
Consider this:
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
On 8/26/25 12:22 PM, Dominik Csapak wrote:
> This adds new panels to the dashboard for showing summaries of the tasks:
> * one by category
> * one by remote
>
> similar to what we have in PBS.
>
> I did not implement a cache for the data on the backend, but this can be
> done as a follow up and should not impact the UI code at all.
>
> changes from v3:
> * instead of having freestanding helper functions, introduce a struct
> with methods that we can pass around, this makes it easier to sort,
> etc. also with this we can get rid of another helper type in the
> tasks ui
> * changed to a btreemap from a hashmap to keep the sorting we get from
> the TaskWorkerType
> * deduplicated the since/hour calculation in the dashboard
>
>
> changes from v2:
> * rebase on current master (incl task caching changes from lukas)
> * new patch (8/8) that includes configuring the time frame to check the
> tasks for
>
> Changes from v1:
> * incorporated most suggestions from Thomas
> * split up patches a bit
> * added new server api call
> * (the UI code is very different to accomodate the necessary changes)
>
>
>
> Dominik Csapak (8):
> server: task cache: treat a limit of 0 as unbounded
> server: api: remote tasks: add 'remote' filter option
> server: api: add remote-tasks statistics
> ui: refactor remote upid formatter
> ui: tasks: add helper to summarize task categories
> ui: add dialog to show filtered tasks
> ui: dashboard: add task summaries
> ui: dashboard: make task summary time range configurable
>
> lib/pdm-api-types/src/lib.rs | 46 +++++
> server/src/api/remote_tasks.rs | 88 ++++++++-
> server/src/remote_tasks/mod.rs | 17 +-
> ui/src/dashboard/filtered_tasks.rs | 291 ++++++++++++++++++++++++++++
> ui/src/dashboard/mod.rs | 128 +++++++++++-
> ui/src/dashboard/tasks.rs | 301 +++++++++++++++++++++++++++++
> ui/src/remotes/tasks.rs | 19 +-
> ui/src/tasks.rs | 103 +++++++++-
> ui/src/top_nav_bar.rs | 18 +-
> 9 files changed, 966 insertions(+), 45 deletions(-)
> create mode 100644 ui/src/dashboard/filtered_tasks.rs
> create mode 100644 ui/src/dashboard/tasks.rs
>
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread
* [pdm-devel] applied-series: [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
` (8 preceding siblings ...)
2025-08-26 12:41 ` [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Stefan Hanreich
@ 2025-09-04 19:21 ` Thomas Lamprecht
9 siblings, 0 replies; 11+ messages in thread
From: Thomas Lamprecht @ 2025-09-04 19:21 UTC (permalink / raw)
To: pdm-devel, Dominik Csapak
On Tue, 26 Aug 2025 12:15:10 +0200, Dominik Csapak wrote:
> This adds new panels to the dashboard for showing summaries of the tasks:
> * one by category
> * one by remote
>
> similar to what we have in PBS.
>
> I did not implement a cache for the data on the backend, but this can be
> done as a follow up and should not impact the UI code at all.
>
> [...]
Applied series, thanks!
[1/8] server: task cache: treat a limit of 0 as unbounded
commit: 39c27eb07c87cc345476d041c621921ef938abf0
[2/8] server: api: remote tasks: add 'remote' filter option
commit: 6cbdb50148a46cfd2eef33c04151e78fe100482a
[3/8] server: api: add remote-tasks statistics
commit: b99d08e266207cee1665531407a8268d56be6b4a
[4/8] ui: refactor remote upid formatter
commit: d50a74f5d6b6b76c53abeb634b6bedd95e45448f
[5/8] ui: tasks: add helper to summarize task categories
commit: f4f2976566e9c18ff3f985062f68c2cb4fc4f0a6
[6/8] ui: add dialog to show filtered tasks
commit: 7ee1472495dfaad99fff7f361cb555975c4f3715
[7/8] ui: dashboard: add task summaries
commit: f172c0a88395715f1a9efae22220319385df64cb
[8/8] ui: dashboard: make task summary time range configurable
commit: bab762dee18b3e603d5418db5258fb18d2657afe
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2025-09-04 19:21 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 1/8] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 2/8] server: api: remote tasks: add 'remote' filter option Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 3/8] server: api: add remote-tasks statistics Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 4/8] ui: refactor remote upid formatter Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 5/8] ui: tasks: add helper to summarize task categories Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 6/8] ui: add dialog to show filtered tasks Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 7/8] ui: dashboard: add task summaries Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 8/8] ui: dashboard: make task summary time range configurable Dominik Csapak
2025-08-26 12:41 ` [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Stefan Hanreich
2025-09-04 19:21 ` [pdm-devel] applied-series: " Thomas Lamprecht
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.