* [pdm-devel] [PATCH datacenter-manager v3 1/8] server: task cache: treat a limit of 0 as unbounded
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
@ 2025-08-25 8:08 ` Dominik Csapak
2025-08-25 9:55 ` Stefan Hanreich
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 2/8] server: api: remote tasks: add 'remote' filter option Dominik Csapak
` (8 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 8:08 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/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] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 1/8] server: task cache: treat a limit of 0 as unbounded
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 1/8] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
@ 2025-08-25 9:55 ` Stefan Hanreich
2025-08-25 10:49 ` Dominik Csapak
0 siblings, 1 reply; 19+ messages in thread
From: Stefan Hanreich @ 2025-08-25 9:55 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
On 8/25/25 10:10 AM, Dominik Csapak wrote:
> 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/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,
> + };
> +
is this a usize because of backwards / API compatibility? Otherwise,
wouldn't an Option be preferrable?
> 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)
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 1/8] server: task cache: treat a limit of 0 as unbounded
2025-08-25 9:55 ` Stefan Hanreich
@ 2025-08-25 10:49 ` Dominik Csapak
2025-08-25 13:40 ` Stefan Hanreich
0 siblings, 1 reply; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 10:49 UTC (permalink / raw)
To: Stefan Hanreich, Proxmox Datacenter Manager development discussion
On 8/25/25 11:55 AM, Stefan Hanreich wrote:
>
>
> On 8/25/25 10:10 AM, Dominik Csapak wrote:
>> 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/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,
>> + };
>> +
>
> is this a usize because of backwards / API compatibility? Otherwise,
> wouldn't an Option be preferrable?
>
i think for the taskfilters it's desireable to have a somehwat sane
default limit in the api (which is currently '50') and make the
'show all tasks' an explicit case, otherwise people rather easily
get all tasks returned, and that can be many
but IMHO this is not set in stone, if we want to return all tasks
by default we could change it of course
since we reuse the `TaskFilters` struct this would also affect the pdm
node tasks themselves.
>> 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)
>
>
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 1/8] server: task cache: treat a limit of 0 as unbounded
2025-08-25 10:49 ` Dominik Csapak
@ 2025-08-25 13:40 ` Stefan Hanreich
0 siblings, 0 replies; 19+ messages in thread
From: Stefan Hanreich @ 2025-08-25 13:40 UTC (permalink / raw)
To: Dominik Csapak, Proxmox Datacenter Manager development discussion
On 8/25/25 12:49 PM, Dominik Csapak wrote:
>
>
> On 8/25/25 11:55 AM, Stefan Hanreich wrote:
>>
>>
>> On 8/25/25 10:10 AM, Dominik Csapak wrote:
>>> 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/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,
>>> + };
>>> +
>>
>> is this a usize because of backwards / API compatibility? Otherwise,
>> wouldn't an Option be preferrable?
>>
>
> i think for the taskfilters it's desireable to have a somehwat sane
> default limit in the api (which is currently '50') and make the
> 'show all tasks' an explicit case, otherwise people rather easily
> get all tasks returned, and that can be many
>
> but IMHO this is not set in stone, if we want to return all tasks
> by default we could change it of course
>
> since we reuse the `TaskFilters` struct this would also affect the pdm
> node tasks themselves.
Yes, that seems reasonable
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 2/8] server: api: remote tasks: add 'remote' filter option
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 1/8] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
@ 2025-08-25 8:08 ` Dominik Csapak
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 3/8] server: api: add remote-tasks statistics Dominik Csapak
` (7 subsequent siblings)
9 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 8:08 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 | 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] 19+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 3/8] server: api: add remote-tasks statistics
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 1/8] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 2/8] server: api: remote tasks: add 'remote' filter option Dominik Csapak
@ 2025-08-25 8:08 ` Dominik Csapak
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 4/8] ui: refactor remote upid formatter Dominik Csapak
` (6 subsequent siblings)
9 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 8:08 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 | 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] 19+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 4/8] ui: refactor remote upid formatter
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
` (2 preceding siblings ...)
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 3/8] server: api: add remote-tasks statistics Dominik Csapak
@ 2025-08-25 8:08 ` Dominik Csapak
2025-08-25 10:56 ` Stefan Hanreich
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 5/8] ui: tasks: add helper to summarize task categories Dominik Csapak
` (5 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 8:08 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 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..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 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] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 4/8] ui: refactor remote upid formatter
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 4/8] ui: refactor remote upid formatter Dominik Csapak
@ 2025-08-25 10:56 ` Stefan Hanreich
2025-08-26 9:48 ` Dominik Csapak
0 siblings, 1 reply; 19+ messages in thread
From: Stefan Hanreich @ 2025-08-25 10:56 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
one comment inline
On 8/25/25 10:10 AM, Dominik Csapak wrote:
> 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 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..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)
> + }
> +}
Maybe some error handling if the &str is not a valid Upid? Maybe we want
an enum / Either<RemoteUpid, PveUpid> type that holds both RemoteUpid /
PveUpid in the future?
> 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()
> }),
> );
>
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 4/8] ui: refactor remote upid formatter
2025-08-25 10:56 ` Stefan Hanreich
@ 2025-08-26 9:48 ` Dominik Csapak
0 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2025-08-26 9:48 UTC (permalink / raw)
To: Stefan Hanreich, Proxmox Datacenter Manager development discussion
On 8/25/25 12:56 PM, Stefan Hanreich wrote:
> one comment inline
>
> On 8/25/25 10:10 AM, Dominik Csapak wrote:
>> 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 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..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)
>> + }
>> +}
>
> Maybe some error handling if the &str is not a valid Upid? Maybe we want
> an enum / Either<RemoteUpid, PveUpid> type that holds both RemoteUpid /
> PveUpid in the future?
>
we simply print the string for now if we can't parse it at all
but yes, we probably want to have a generic upid parsing that can parse
all 4 upids we have:
RemoteUpid with pbs upid
RemoteUpid with pve upid
plain pve upid
plain pbs upid
for pve vs pbs we already have a helper in proxmox-yew-comp, but imho
this should either be a separate crate (since we'll want this in pdm
server too) or should live in proxmox-schema (or similar)
for now i think this is ok, especially this is how we do
it for all other views too (if we can't parse print the upid itself)
>> 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()
>> }),
>> );
>>
>
>
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 5/8] ui: tasks: add helper to summarize task categories
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
` (3 preceding siblings ...)
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 4/8] ui: refactor remote upid formatter Dominik Csapak
@ 2025-08-25 8:08 ` Dominik Csapak
2025-08-25 9:54 ` Stefan Hanreich
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 6/8] ui: add dialog to show filtered tasks Dominik Csapak
` (4 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 8:08 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.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] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 5/8] ui: tasks: add helper to summarize task categories
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 5/8] ui: tasks: add helper to summarize task categories Dominik Csapak
@ 2025-08-25 9:54 ` Stefan Hanreich
0 siblings, 0 replies; 19+ messages in thread
From: Stefan Hanreich @ 2025-08-25 9:54 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
On 8/25/25 10:10 AM, Dominik Csapak wrote:
> 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(),
> + }
> +}
maybe better suited as an enum with FromStr(or From<_> since it's
infallible) + Display?
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 6/8] ui: add dialog to show filtered tasks
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
` (4 preceding siblings ...)
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 5/8] ui: tasks: add helper to summarize task categories Dominik Csapak
@ 2025-08-25 8:08 ` Dominik Csapak
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 7/8] ui: dashboard: add task summaries Dominik Csapak
` (3 subsequent siblings)
9 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 8:08 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 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..748688d
--- /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, 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, 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,
+ };
+ 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] 19+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 7/8] ui: dashboard: add task summaries
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
` (5 preceding siblings ...)
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 6/8] ui: add dialog to show filtered tasks Dominik Csapak
@ 2025-08-25 8:08 ` Dominik Csapak
2025-08-25 10:52 ` Stefan Hanreich
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 8/8] ui: dashboard: make task summary time range configurable Dominik Csapak
` (2 subsequent siblings)
9 siblings, 1 reply; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 8:08 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 | 108 ++++++++++++-
ui/src/dashboard/tasks.rs | 316 ++++++++++++++++++++++++++++++++++++++
2 files changed, 418 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..efd43d2
--- /dev/null
+++ b/ui/src/dashboard/tasks.rs
@@ -0,0 +1,316 @@
+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(),
+ ])
+ }
+
+ 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)| {
+ 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.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] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 7/8] ui: dashboard: add task summaries
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 7/8] ui: dashboard: add task summaries Dominik Csapak
@ 2025-08-25 10:52 ` Stefan Hanreich
0 siblings, 0 replies; 19+ messages in thread
From: Stefan Hanreich @ 2025-08-25 10:52 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
one comment inline
On 8/25/25 10:10 AM, Dominik Csapak wrote:
> 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 | 108 ++++++++++++-
> ui/src/dashboard/tasks.rs | 316 ++++++++++++++++++++++++++++++++++++++
> 2 files changed, 418 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..efd43d2
> --- /dev/null
> +++ b/ui/src/dashboard/tasks.rs
> @@ -0,0 +1,316 @@
> +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
> +}
If we move the type into an enum, as suggested previously, then we could
just use a BTreeMap and get sorting/prio for free?
Could also just insert entries into the map 'on demand' unless we want
to show all headings in the UI?
> +
> +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(),
> + ])
> + }
> +
> + 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)| {
> + 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()
> + }
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 8/8] ui: dashboard: make task summary time range configurable
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
` (6 preceding siblings ...)
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 7/8] ui: dashboard: add task summaries Dominik Csapak
@ 2025-08-25 8:08 ` Dominik Csapak
2025-08-25 9:54 ` Stefan Hanreich
2025-08-25 13:17 ` [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Stefan Hanreich
2025-08-26 11:22 ` [pdm-devel] superseded: " Dominik Csapak
9 siblings, 1 reply; 19+ messages in thread
From: Dominik Csapak @ 2025-08-25 8:08 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>
---
ui/src/dashboard/mod.rs | 34 ++++++++++++++++++++++++++++++----
1 file changed, 30 insertions(+), 4 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 502332a..d9b58b6 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 = (
@@ -257,6 +262,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 last_hours = self.statistics.hours;
self.load_finished_time = None;
self.async_pool.spawn(async move {
@@ -265,7 +271,7 @@ 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 since = (Date::now() / 1000.0) as i64 - (last_hours * 60 * 60) as i64;
let params = Some(json!({
"since": since,
"limit": 0,
@@ -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,9 +306,10 @@ 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 hours = config.task_last_hours.unwrap_or(DEFAULT_TASK_SUMMARY_HOURS);
+ let since = (Date::now() / 1000.0) as i64 - (hours * 60 * 60) as i64;
let statistics = StatisticsOptions {
- hours: 24,
+ hours,
since,
data: None,
error: None,
@@ -375,6 +383,17 @@ impl Component for PdmDashboard {
}
Msg::UpdateConfig(dashboard_config) => {
self.config.update(dashboard_config);
+ let old_hours = self.statistics.hours;
+ let new_hours = self
+ .config
+ .task_last_hours
+ .unwrap_or(DEFAULT_TASK_SUMMARY_HOURS);
+
+ if old_hours != new_hours {
+ self.statistics.hours = new_hours;
+ self.reload(ctx);
+ }
+
self.show_config_window = false;
true
}
@@ -592,6 +611,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] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 8/8] ui: dashboard: make task summary time range configurable
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 8/8] ui: dashboard: make task summary time range configurable Dominik Csapak
@ 2025-08-25 9:54 ` Stefan Hanreich
0 siblings, 0 replies; 19+ messages in thread
From: Stefan Hanreich @ 2025-08-25 9:54 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
On 8/25/25 10:11 AM, Dominik Csapak wrote:
> 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>
> ---
> ui/src/dashboard/mod.rs | 34 ++++++++++++++++++++++++++++++----
> 1 file changed, 30 insertions(+), 4 deletions(-)
>
> diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
> index 502332a..d9b58b6 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 = (
> @@ -257,6 +262,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 last_hours = self.statistics.hours;
>
> self.load_finished_time = None;
> self.async_pool.spawn(async move {
> @@ -265,7 +271,7 @@ 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 since = (Date::now() / 1000.0) as i64 - (last_hours * 60 * 60) as i64;
> let params = Some(json!({
> "since": since,
> "limit": 0,
> @@ -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,9 +306,10 @@ 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 hours = config.task_last_hours.unwrap_or(DEFAULT_TASK_SUMMARY_HOURS);
nit: maybe make this a getter? (max_age could be as well?)
> + let since = (Date::now() / 1000.0) as i64 - (hours * 60 * 60) as i64;
possibly this as well? or at least extract into a function?
> let statistics = StatisticsOptions {
> - hours: 24,
> + hours,
> since,
> data: None,
> error: None,
> @@ -375,6 +383,17 @@ impl Component for PdmDashboard {
> }
> Msg::UpdateConfig(dashboard_config) => {
> self.config.update(dashboard_config);
> + let old_hours = self.statistics.hours;
> + let new_hours = self
> + .config
> + .task_last_hours
> + .unwrap_or(DEFAULT_TASK_SUMMARY_HOURS);
duplicated here
> +
> + if old_hours != new_hours {
> + self.statistics.hours = new_hours;
> + self.reload(ctx);
> + }
> +
> self.show_config_window = false;
> true
> }
> @@ -592,6 +611,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)))
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
` (7 preceding siblings ...)
2025-08-25 8:08 ` [pdm-devel] [PATCH datacenter-manager v3 8/8] ui: dashboard: make task summary time range configurable Dominik Csapak
@ 2025-08-25 13:17 ` Stefan Hanreich
2025-08-26 11:22 ` [pdm-devel] superseded: " Dominik Csapak
9 siblings, 0 replies; 19+ messages in thread
From: Stefan Hanreich @ 2025-08-25 13:17 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
In the review nothing major came up, and the suggestions could also be
added as follow-ups if desired.
Also gave this a quick test on my PDM instance, so consider this:
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
On 8/25/25 10:11 AM, 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 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 | 297 +++++++++++++++++++++++++++
> ui/src/dashboard/mod.rs | 138 ++++++++++++-
> ui/src/dashboard/tasks.rs | 316 +++++++++++++++++++++++++++++
> ui/src/remotes/tasks.rs | 19 +-
> ui/src/tasks.rs | 52 ++++-
> ui/src/top_nav_bar.rs | 18 +-
> 9 files changed, 946 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] 19+ messages in thread
* [pdm-devel] superseded: [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard
2025-08-25 8:08 [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Dominik Csapak
` (8 preceding siblings ...)
2025-08-25 13:17 ` [pdm-devel] [PATCH datacenter-manager v3 0/8] add task summary panels in dashboard Stefan Hanreich
@ 2025-08-26 11:22 ` Dominik Csapak
9 siblings, 0 replies; 19+ messages in thread
From: Dominik Csapak @ 2025-08-26 11:22 UTC (permalink / raw)
To: pdm-devel
superseded by v4:
https://lore.proxmox.com/pdm-devel/20250826102229.2271453-1-d.csapak@proxmox.com/
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 19+ messages in thread