public inbox for pdm-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard
@ 2025-02-19 12:28 Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 1/7] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
                   ` (6 more replies)
  0 siblings, 7 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-02-19 12:28 UTC (permalink / raw)
  To: pdm-devel

This is a replacement series for my patch:
https://lore.proxmox.com/pdm-devel/20250123151012.4047891-1-d.csapak@proxmox.com/

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.

This series might conflict a bit with Lukas approach to the task
caching on the backend, but I'm happy to rebase my patches on top of
his, when they're applied.

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.

Also I might use that chance to implement a generic in-memory cache for
our api calls, so that we don't have to implement it for every part we
want to cache.

Also on the UI customizable filters are still missing for the filtered
list, but those will be done as a follow up by me. (IMHO it's already
very usable)

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 (7):
  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

 lib/pdm-api-types/src/lib.rs       |  60 ++++++
 server/src/api/remote_tasks.rs     |  97 ++++++++-
 server/src/task_cache.rs           |  18 +-
 ui/src/dashboard/filtered_tasks.rs | 297 ++++++++++++++++++++++++++++
 ui/src/dashboard/mod.rs            | 112 ++++++++++-
 ui/src/dashboard/tasks.rs          | 302 +++++++++++++++++++++++++++++
 ui/src/remotes/tasks.rs            |  19 +-
 ui/src/tasks.rs                    |  52 ++++-
 ui/src/top_nav_bar.rs              |  18 +-
 9 files changed, 932 insertions(+), 43 deletions(-)
 create mode 100644 ui/src/dashboard/filtered_tasks.rs
 create mode 100644 ui/src/dashboard/tasks.rs

-- 
2.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 1/7] server: task cache: treat a limit of 0 as unbounded
  2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
@ 2025-02-19 12:28 ` Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 2/7] server: api: remote tasks: add 'remote' filter option Dominik Csapak
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-02-19 12:28 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>
---
 server/src/task_cache.rs | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/server/src/task_cache.rs b/server/src/task_cache.rs
index 7ded540..4c34827 100644
--- a/server/src/task_cache.rs
+++ b/server/src/task_cache.rs
@@ -56,6 +56,11 @@ pub async fn get_tasks(max_age: i64, filters: TaskFilters) -> Result<Vec<TaskLis
         }
     }
 
+    let limit = match filters.limit {
+        0 => usize::MAX,
+        limit => limit as usize,
+    };
+
     let mut returned_tasks = add_running_tasks(all_tasks)?;
     returned_tasks.sort_by(|a, b| b.starttime.cmp(&a.starttime));
     let returned_tasks = returned_tasks
@@ -105,7 +110,7 @@ pub async fn get_tasks(max_age: i64, filters: TaskFilters) -> Result<Vec<TaskLis
             true
         })
         .skip(filters.start as usize)
-        .take(filters.limit as usize)
+        .take(limit)
         .collect();
 
     // We don't need to wait for this task to finish
-- 
2.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 2/7] server: api: remote tasks: add 'remote' filter option
  2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 1/7] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
@ 2025-02-19 12:28 ` Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 3/7] server: api: add remote-tasks statistics Dominik Csapak
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-02-19 12:28 UTC (permalink / raw)
  To: pdm-devel

so we can filter the tasks by remote

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 server/src/api/remote_tasks.rs | 17 +++++++++++++----
 server/src/task_cache.rs       | 11 ++++++++++-
 2 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/server/src/api/remote_tasks.rs b/server/src/api/remote_tasks.rs
index 57b59fd..d327272 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;
@@ -31,13 +32,21 @@ 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(max_age: i64, filters: TaskFilters) -> Result<Vec<TaskListItem>, Error> {
-    let tasks = task_cache::get_tasks(max_age, filters).await?;
+async fn list_tasks(
+    max_age: i64,
+    filters: TaskFilters,
+    remote: Option<String>,
+) -> Result<Vec<TaskListItem>, Error> {
+    let tasks = task_cache::get_tasks(max_age, filters, remote).await?;
 
     Ok(tasks)
 }
diff --git a/server/src/task_cache.rs b/server/src/task_cache.rs
index 4c34827..c5e8956 100644
--- a/server/src/task_cache.rs
+++ b/server/src/task_cache.rs
@@ -20,7 +20,11 @@ use crate::{api::pve, task_utils};
 
 /// Get tasks for all remotes
 // FIXME: filter for privileges
-pub async fn get_tasks(max_age: i64, filters: TaskFilters) -> Result<Vec<TaskListItem>, Error> {
+pub async fn get_tasks(
+    max_age: i64,
+    filters: TaskFilters,
+    remote_filter: Option<String>,
+) -> Result<Vec<TaskListItem>, Error> {
     let (remotes, _) = pdm_config::remotes::config()?;
 
     let mut all_tasks = Vec::new();
@@ -37,6 +41,11 @@ pub async fn get_tasks(max_age: i64, filters: TaskFilters) -> Result<Vec<TaskLis
     invalidate_cache_for_finished_tasks(&mut cache);
 
     for (remote_name, remote) in &remotes.sections {
+        if let Some(remote_filter) = &remote_filter {
+            if remote_name != remote_filter {
+                continue;
+            }
+        }
         let now = proxmox_time::epoch_i64();
 
         if let Some(tasks) = cache.get_tasks(remote_name.as_str(), now, max_age) {
-- 
2.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 3/7] server: api: add remote-tasks statistics
  2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 1/7] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 2/7] server: api: remote tasks: add 'remote' filter option Dominik Csapak
@ 2025-02-19 12:28 ` Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 4/7] ui: refactor remote upid formatter Dominik Csapak
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-02-19 12:28 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>
---
 lib/pdm-api-types/src/lib.rs   | 55 +++++++++++++++++++++++
 server/src/api/remote_tasks.rs | 82 +++++++++++++++++++++++++++++++++-
 2 files changed, 135 insertions(+), 2 deletions(-)

diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index 3844907..47e5894 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};
@@ -232,6 +233,20 @@ pub enum TaskStateType {
     Unknown,
 }
 
+impl From<&str> for TaskStateType {
+    fn from(s: &str) -> Self {
+        if s == "OK" {
+            TaskStateType::OK
+        } else if s.starts_with("WARNINGS: ") {
+            TaskStateType::Warning
+        } else if !s.is_empty() {
+            TaskStateType::Error
+        } else {
+            TaskStateType::Unknown
+        }
+    }
+}
+
 #[api(
     properties: {
         upid: { schema: UPID::API_SCHEMA },
@@ -263,6 +278,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 d327272..28def2a 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
@@ -50,3 +61,70 @@ 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: {
+            "max-age": {
+                type: Integer,
+                optional: true,
+                // TODO: sensible default max-age
+                default: 300,
+                description: "Maximum age of cached task data",
+            },
+            filters: {
+                type: TaskFilters,
+                flatten: true,
+            },
+            remote: {
+                schema: REMOTE_ID_SCHEMA,
+                optional: true,
+            },
+        },
+    },
+)]
+/// Get task statistics for the specified filters.
+async fn task_statistics(
+    max_age: i64,
+    filters: TaskFilters,
+    remote: Option<String>,
+) -> Result<TaskStatistics, Error> {
+    let tasks = task_cache::get_tasks(max_age, 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) => status.into(),
+            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.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 4/7] ui: refactor remote upid formatter
  2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
                   ` (2 preceding siblings ...)
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 3/7] server: api: add remote-tasks statistics Dominik Csapak
@ 2025-02-19 12:28 ` Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 5/7] ui: tasks: add helper to summarize task categories Dominik Csapak
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-02-19 12:28 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>
---
 ui/src/remotes/tasks.rs | 19 ++++---------------
 ui/src/tasks.rs         | 23 ++++++++++++++++++++++-
 ui/src/top_nav_bar.rs   | 18 +++---------------
 3 files changed, 29 insertions(+), 31 deletions(-)

diff --git a/ui/src/remotes/tasks.rs b/ui/src/remotes/tasks.rs
index 99bb243..e74ebf2 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,
@@ -24,6 +21,8 @@ use pwt::{
     },
 };
 
+use crate::tasks::format_optional_remote_upid;
+
 #[derive(PartialEq, Properties)]
 pub struct RemoteTaskList;
 impl RemoteTaskList {
@@ -74,17 +73,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..0e7899c 100644
--- a/ui/src/tasks.rs
+++ b/ui/src/tasks.rs
@@ -1,6 +1,9 @@
-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;
+
 pub fn register_pve_tasks() {
     register_task_description("qmstart", ("VM", tr!("Start")));
     register_task_description("acmedeactivate", ("ACME Account", tr!("Deactivate")));
@@ -99,3 +102,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 07b3b23..88ff137 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.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 5/7] ui: tasks: add helper to summarize task categories
  2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
                   ` (3 preceding siblings ...)
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 4/7] ui: refactor remote upid formatter Dominik Csapak
@ 2025-02-19 12:28 ` Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 6/7] ui: add dialog to show filtered tasks Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 7/7] ui: dashboard: add task summaries Dominik Csapak
  6 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-02-19 12:28 UTC (permalink / raw)
  To: pdm-devel

one for mapping UPID worker types to shortahnds that can be used for
pre-filtering via the api (have to filter on the client side a bit more
since we'll return more than what we want with the current filtering
api)

and one for creating a title for each type

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 ui/src/tasks.rs | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/ui/src/tasks.rs b/ui/src/tasks.rs
index 0e7899c..a94cc9a 100644
--- a/ui/src/tasks.rs
+++ b/ui/src/tasks.rs
@@ -120,3 +120,32 @@ 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)
+///
+/// Note: if using as filter for the api, 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 map_worker_type(worker_type: &str) -> &str {
+    match worker_type {
+        task_type if task_type.contains("migrate") => "migrate",
+        task_type if task_type.starts_with("qm") => "qm",
+        task_type if task_type.starts_with("vz") && task_type != "vzdump" => "vz",
+        task_type if task_type.starts_with("ceph") => "ceph",
+        task_type if task_type.starts_with("ha") => "ha",
+        other => other,
+    }
+}
+
+/// Map a category from [`map_worker_type`] to a title text.
+pub fn get_type_title(task_type: &str) -> String {
+    match task_type {
+        "migrate" => tr!("Guest Migrations"),
+        "qm" => tr!("Virtual Machine related Tasks"),
+        "vz" => tr!("Container related Tasks"),
+        "ceph" => tr!("Ceph related Tasks"),
+        "vzdump" => tr!("Backup Tasks"),
+        "ha" => tr!("HA related Tasks"),
+        other => other.to_string(),
+    }
+}
-- 
2.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 6/7] ui: add dialog to show filtered tasks
  2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
                   ` (4 preceding siblings ...)
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 5/7] ui: tasks: add helper to summarize task categories Dominik Csapak
@ 2025-02-19 12:28 ` Dominik Csapak
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 7/7] ui: dashboard: add task summaries Dominik Csapak
  6 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-02-19 12:28 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>
---
 lib/pdm-api-types/src/lib.rs       |   5 +
 ui/src/dashboard/filtered_tasks.rs | 297 +++++++++++++++++++++++++++++
 ui/src/dashboard/mod.rs            |   2 +
 3 files changed, 304 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 47e5894..ccf1d43 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -546,9 +546,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..c8cbf34
--- /dev/null
+++ b/ui/src/dashboard/filtered_tasks.rs
@@ -0,0 +1,297 @@
+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, Mask, Tooltip,
+    },
+    AsyncPool,
+};
+use pwt::{state::Store, tr, widget::Dialog};
+
+use pdm_api_types::{RemoteUpid, TaskFilters, TaskStateType};
+
+use crate::tasks::{format_optional_remote_upid, get_type_title, map_worker_type};
+
+#[derive(PartialEq, Clone)]
+pub enum TaskGroup {
+    Remote(String), // remote name
+    Type(String),   // worker type
+}
+
+#[derive(PartialEq, Properties)]
+#[builder]
+pub struct FilteredTasks {
+    grouping: TaskGroup,
+    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: TaskGroup, 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: TaskGroup,
+    ) -> 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,
+        };
+
+        if let TaskGroup::Type(worker_type) = &grouping {
+            filters.typefilter = Some(worker_type.to_string());
+        }
+
+        let mut params = serde_json::to_value(filters)?;
+
+        if let TaskGroup::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() {
+                    TaskGroup::Remote(_) => {}
+                    TaskGroup::Type(worker_type) => {
+                        self.task_store.set_filter(move |entry: &TaskListItem| {
+                            worker_type == map_worker_type(&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!(
+                "{} - {}",
+                match &props.grouping {
+                    TaskGroup::Remote(remote) => remote.to_string(),
+                    TaskGroup::Type(worker_type) => get_type_title(worker_type),
+                },
+                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,
+                };
+                icon.to_fa_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 ea0cf5e..9d79cd3 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -26,6 +26,8 @@ pub use top_entities::TopEntities;
 mod subscription_info;
 pub use subscription_info::SubscriptionInfo;
 
+mod filtered_tasks;
+
 #[derive(Properties, PartialEq)]
 pub struct Dashboard {
     #[prop_or(60)]
-- 
2.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [pdm-devel] [PATCH datacenter-manager v2 7/7] ui: dashboard: add task summaries
  2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
                   ` (5 preceding siblings ...)
  2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 6/7] ui: add dialog to show filtered tasks Dominik Csapak
@ 2025-02-19 12:28 ` Dominik Csapak
  6 siblings, 0 replies; 8+ messages in thread
From: Dominik Csapak @ 2025-02-19 12:28 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>
---
 ui/src/dashboard/mod.rs   | 110 +++++++++++++-
 ui/src/dashboard/tasks.rs | 302 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 407 insertions(+), 5 deletions(-)
 create mode 100644 ui/src/dashboard/tasks.rs

diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 9d79cd3..62237b6 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -1,6 +1,7 @@
 use std::rc::Rc;
 
 use anyhow::Error;
+use js_sys::Date;
 use serde_json::json;
 use yew::{
     virtual_dom::{VComp, VNode},
@@ -15,7 +16,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 crate::{remotes::AddWizard, RemoteList};
@@ -28,6 +32,9 @@ pub use subscription_info::SubscriptionInfo;
 
 mod filtered_tasks;
 
+mod tasks;
+use tasks::TaskSummary;
+
 #[derive(Properties, PartialEq)]
 pub struct Dashboard {
     #[prop_or(60)]
@@ -50,15 +57,24 @@ impl Default for Dashboard {
 pub enum Msg {
     LoadingFinished(Result<ResourcesStatus, Error>),
     TopEntitiesLoadResult(Result<pdm_client::types::TopEntities, proxmox_client::Error>),
+    StatisticsLoadResult(Result<TaskStatistics, Error>),
     RemoteListChanged(RemoteList),
     CreateWizard(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,
     remote_list: RemoteList,
     show_wizard: bool,
@@ -163,6 +179,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,16 +266,39 @@ impl Component for PdmDashboard {
                 link.send_message(Msg::TopEntitiesLoadResult(result));
             }
         });
+        let hours = 24;
+        let since = (Date::now() / 1000.0) as i64 - (hours as i64 * 60 * 60);
+        async_pool.spawn({
+            let link = ctx.link().clone();
+            async move {
+                // TODO replace with pdm client call
+                let params = Some(json!({
+                    "since": since,
+                    "limit": 0,
+                }));
+
+                let res = http_get("/remote-tasks/statistics", params).await;
+                link.send_message(Msg::StatisticsLoadResult(res));
+            }
+        });
         let (remote_list, _context_listener) = ctx
             .link()
             .context(ctx.link().callback(Msg::RemoteListChanged))
             .expect("No Remote list context provided");
 
+        let statistics = StatisticsOptions {
+            hours,
+            since,
+            data: None,
+            error: None,
+        };
+
         Self {
             status: ResourcesStatus::default(),
             last_error: None,
             top_entities: None,
             last_top_entities_error: None,
+            statistics,
             loading: true,
             remote_list,
             show_wizard: false,
@@ -250,6 +330,16 @@ impl Component for PdmDashboard {
                 }
                 true
             }
+            Msg::StatisticsLoadResult(res) => {
+                match res {
+                    Ok(statistics) => {
+                        self.statistics.error = None;
+                        self.statistics.data = Some(statistics);
+                    }
+                    Err(err) => self.statistics.error = Some(err),
+                }
+                true
+            }
             Msg::RemoteListChanged(remote_list) => {
                 let changed = self.remote_list != remote_list;
                 self.remote_list = remote_list;
@@ -262,7 +352,7 @@ impl Component for PdmDashboard {
         }
     }
 
-    fn view(&self, _ctx: &yew::Context<Self>) -> yew::Html {
+    fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
         let (remote_icon, remote_text) = match (self.status.failed_remotes, self.status.remotes) {
             (0, 0) => (Status::Warning.to_fa_icon(), tr!("No remotes configured.")),
             (0, _) => (
@@ -292,7 +382,7 @@ impl Component for PdmDashboard {
                             .with_tool(
                                 Button::new(tr!("Add"))
                                     .icon_class("fa fa-plus-circle")
-                                    .onclick(_ctx.link().callback(|_| Msg::CreateWizard(true))),
+                                    .onclick(ctx.link().callback(|_| Msg::CreateWizard(true))),
                             )
                             .with_child(
                                 Column::new()
@@ -394,7 +484,6 @@ impl Component for PdmDashboard {
                     .class("pwt-content-spacer")
                     .class("pwt-flex-direction-row")
                     .class("pwt-align-content-start")
-                    .class(pwt::css::Flex::Fill)
                     .style("padding-top", "0")
                     .class(FlexWrap::Wrap)
                     //.min_height(175)
@@ -416,6 +505,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("pwt-flex-direction-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()
@@ -425,7 +525,7 @@ impl Component for PdmDashboard {
             .with_optional_child(
                 self.show_wizard.then_some(
                     AddWizard::new(pdm_api_types::remotes::RemoteType::Pve)
-                        .on_close(_ctx.link().callback(|_| Msg::CreateWizard(false)))
+                        .on_close(ctx.link().callback(|_| Msg::CreateWizard(false)))
                         .on_submit(move |ctx| {
                             crate::remotes::create_remote(
                                 ctx,
diff --git a/ui/src/dashboard/tasks.rs b/ui/src/dashboard/tasks.rs
new file mode 100644
index 0000000..a41b38a
--- /dev/null
+++ b/ui/src/dashboard/tasks.rs
@@ -0,0 +1,302 @@
+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::{get_type_title, map_worker_type};
+
+use super::filtered_tasks::FilteredTasks;
+
+#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
+struct TaskSummaryItem {
+    prio: usize, // sort order
+    group: String,
+    title: String,
+    error_count: u64,
+    warning_count: u64,
+    ok_count: u64,
+    unknown_count: u64,
+}
+
+impl TaskSummaryItem {
+    fn new(group: impl Into<String>, title: impl Into<String>, prio: usize) -> Self {
+        TaskSummaryItem {
+            group: group.into(),
+            title: title.into(),
+            prio,
+            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<(String, TaskStatusClass)>), // task_tyope
+}
+
+#[derive(PartialEq)]
+pub enum ViewState {}
+
+#[doc(hidden)]
+pub struct ProxmoxTaskSummary {
+    store: Store<TaskSummaryItem>,
+    task_filters: Option<(String, TaskStatusClass)>,
+}
+
+fn extract_task_summary(data: &TaskStatistics) -> Vec<TaskSummaryItem> {
+    let mut map: HashMap<String, TaskSummaryItem> = HashMap::new();
+
+    let mut prio = 0;
+    let mut insert_type = |task_type: &str| {
+        prio += 1;
+        map.insert(
+            task_type.to_string(),
+            TaskSummaryItem::new(task_type, get_type_title(task_type), prio),
+        );
+    };
+
+    insert_type("migrate");
+    insert_type("qm");
+    insert_type("vz");
+    insert_type("ceph");
+    insert_type("vzdump");
+    insert_type("ha");
+
+    for (worker_type, count) in data.by_type.iter() {
+        let task_type = map_worker_type(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;
+    }
+
+    let mut list: Vec<TaskSummaryItem> = map.into_values().collect();
+    list.sort();
+    list
+}
+
+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(remote, remote, 0));
+
+        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: String,
+    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}", get_type_title(&task_type), 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(|item: &TaskSummaryItem| &item.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(),
+        ])
+    }
+}
+
+impl Component for ProxmoxTaskSummary {
+    type Properties = TaskSummary;
+    type Message = Msg;
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let store = Store::new();
+        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));
+        }
+
+        Self {
+            store,
+            task_filters: None,
+        }
+    }
+
+    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Msg::ShowFilteredTasks(filters) => {
+                self.task_filters = filters;
+            }
+        }
+        true
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let props = ctx.props();
+        let tasks = self.task_filters.as_ref().map(|(task_type, task_status)| {
+            let group = if props.top_remotes.is_some() {
+                super::filtered_tasks::TaskGroup::Remote(task_type.clone())
+            } else {
+                super::filtered_tasks::TaskGroup::Type(task_type.clone())
+            };
+            FilteredTasks::new(props.since, group, *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.39.5



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2025-02-19 12:29 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 1/7] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 2/7] server: api: remote tasks: add 'remote' filter option Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 3/7] server: api: add remote-tasks statistics Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 4/7] ui: refactor remote upid formatter Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 5/7] ui: tasks: add helper to summarize task categories Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 6/7] ui: add dialog to show filtered tasks Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 7/7] ui: dashboard: add task summaries Dominik Csapak

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal