From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v4 6/8] ui: add dialog to show filtered tasks
Date: Tue, 26 Aug 2025 12:15:16 +0200 [thread overview]
Message-ID: <20250826102229.2271453-7-d.csapak@proxmox.com> (raw)
In-Reply-To: <20250826102229.2271453-1-d.csapak@proxmox.com>
This is a dialog that gets and shows a list of filtered tasks, filtered
either by UPID worker types or remotes and always a state (so
success,warning or error)
This needs a bit of adaption for the serializer of TaskFilters, so
we can use it to generate the parameters.
Not used yet here, but we'll use it in the dashboard task summary
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v3:
* use TaskWorkerType instead of helpers
* remove TaskGroup helper struct
lib/pdm-api-types/src/lib.rs | 5 +
ui/src/dashboard/filtered_tasks.rs | 291 +++++++++++++++++++++++++++++
ui/src/dashboard/mod.rs | 2 +
3 files changed, 298 insertions(+)
create mode 100644 ui/src/dashboard/filtered_tasks.rs
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index c506544..e31a904 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -549,9 +549,14 @@ pub struct TaskFilters {
pub errors: bool,
#[serde(default)]
pub running: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub userfilter: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub since: Option<i64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub until: Option<i64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub typefilter: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub statusfilter: Option<Vec<TaskStateType>>,
}
diff --git a/ui/src/dashboard/filtered_tasks.rs b/ui/src/dashboard/filtered_tasks.rs
new file mode 100644
index 0000000..6f211c2
--- /dev/null
+++ b/ui/src/dashboard/filtered_tasks.rs
@@ -0,0 +1,291 @@
+use std::rc::Rc;
+
+use anyhow::Error;
+use proxmox_yew_comp::{
+ common_api_types::{TaskListItem, TaskStatusClass},
+ http_get,
+ utils::{format_duration_human, render_epoch},
+ Status, TaskViewer,
+};
+use pwt_macros::builder;
+use yew::{
+ html::IntoEventCallback,
+ virtual_dom::{VComp, VNode},
+ Component, Properties,
+};
+
+use pwt::{
+ css::FlexFit,
+ prelude::*,
+ widget::{
+ data_table::{DataTable, DataTableColumn, DataTableHeader},
+ ActionIcon, AlertDialog, Fa, Mask, Tooltip,
+ },
+ AsyncPool,
+};
+use pwt::{state::Store, tr, widget::Dialog};
+
+use pdm_api_types::{RemoteUpid, TaskFilters, TaskStateType};
+
+use crate::tasks::{format_optional_remote_upid, TaskWorkerType};
+
+#[derive(PartialEq, Properties)]
+#[builder]
+pub struct FilteredTasks {
+ grouping: TaskWorkerType,
+ task_status: TaskStatusClass,
+ since: i64,
+
+ #[prop_or_default]
+ #[builder_cb(IntoEventCallback, into_event_callback, ())]
+ /// Callback for closing the Dialog
+ on_close: Option<Callback<()>>,
+}
+
+impl FilteredTasks {
+ /// Create new instance with filters for task type and status, beginning from 'since'
+ pub fn new(since: i64, grouping: TaskWorkerType, task_status: TaskStatusClass) -> Self {
+ yew::props!(Self {
+ since,
+ grouping,
+ task_status,
+ })
+ }
+}
+
+impl From<FilteredTasks> for VNode {
+ fn from(val: FilteredTasks) -> Self {
+ let comp = VComp::new::<PdmFilteredTasks>(Rc::new(val), None);
+ VNode::from(comp)
+ }
+}
+
+pub enum Msg {
+ LoadFinished(Result<Vec<TaskListItem>, Error>),
+ ShowTask(Option<(RemoteUpid, Option<i64>)>),
+}
+
+pub struct PdmFilteredTasks {
+ task_store: Store<TaskListItem>,
+ task_info: Option<(RemoteUpid, Option<i64>)>,
+ loading: bool,
+ last_error: Option<Error>,
+ _async_pool: AsyncPool,
+}
+
+impl PdmFilteredTasks {
+ async fn load(
+ since: i64,
+ status: TaskStatusClass,
+ grouping: TaskWorkerType,
+ ) -> Result<Vec<TaskListItem>, Error> {
+ // TODO replace with pdm client call
+ let status = match status {
+ TaskStatusClass::Ok => TaskStateType::OK,
+ TaskStatusClass::Warning => TaskStateType::Warning,
+ TaskStatusClass::Error => TaskStateType::Error,
+ };
+ let mut filters = TaskFilters {
+ since: Some(since),
+ limit: 0,
+ userfilter: None,
+ until: None,
+ typefilter: None,
+ statusfilter: Some(vec![status.clone()]),
+
+ start: 0,
+ errors: false,
+ running: false,
+ };
+
+ match &grouping {
+ TaskWorkerType::Remote(_) => {}
+ worker_type => {
+ filters.typefilter = Some(worker_type.to_filter().to_string());
+ }
+ }
+
+ let mut params = serde_json::to_value(filters)?;
+
+ if let TaskWorkerType::Remote(remote) = grouping {
+ params["remote"] = serde_json::Value::String(remote);
+ }
+
+ http_get("/remote-tasks/list", Some(params)).await
+ }
+}
+
+impl Component for PdmFilteredTasks {
+ type Message = Msg;
+ type Properties = FilteredTasks;
+
+ fn create(ctx: &Context<Self>) -> Self {
+ let props = ctx.props();
+ let since = props.since;
+ let grouping = props.grouping.clone();
+ let status = props.task_status;
+ let link = ctx.link().clone();
+ let _async_pool = AsyncPool::new();
+ _async_pool.send_future(link, async move {
+ let res = Self::load(since, status, grouping).await;
+ Msg::LoadFinished(res)
+ });
+ Self {
+ task_store: Store::new(),
+ task_info: None,
+ loading: true,
+ last_error: None,
+ _async_pool,
+ }
+ }
+
+ fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::LoadFinished(Ok(task_list_items)) => {
+ self.last_error = None;
+ self.loading = false;
+ self.task_store.set_data(task_list_items);
+ match _ctx.props().grouping.clone() {
+ TaskWorkerType::Remote(_) => {}
+ worker_type => {
+ self.task_store.set_filter(move |entry: &TaskListItem| {
+ worker_type == TaskWorkerType::new_from_str(&entry.worker_type)
+ });
+ }
+ }
+ }
+ Msg::LoadFinished(Err(err)) => {
+ self.loading = false;
+ self.last_error = Some(err);
+ }
+ Msg::ShowTask(task) => {
+ self.task_info = task;
+ }
+ }
+ true
+ }
+
+ fn view(&self, ctx: &Context<Self>) -> Html {
+ let props = ctx.props();
+ if let Some(err) = &self.last_error {
+ return AlertDialog::new(err.to_string())
+ .on_close(ctx.props().on_close.clone())
+ .into();
+ }
+
+ if let Some((upid, endtime)) = &self.task_info {
+ // TODO PBS
+ let base_url = format!("/pve/remotes/{}/tasks", upid.remote());
+ TaskViewer::new(upid.to_string())
+ .endtime(endtime)
+ .base_url(base_url)
+ .on_close({
+ let link = ctx.link().clone();
+ move |_| link.send_message(Msg::ShowTask(None))
+ })
+ .into()
+ } else {
+ let title = format!(
+ "{} - {}",
+ props.grouping.to_title(),
+ match props.task_status {
+ TaskStatusClass::Ok => tr!("OK"),
+ TaskStatusClass::Warning => tr!("Warning"),
+ TaskStatusClass::Error => tr!("Error"),
+ },
+ );
+ Dialog::new(title)
+ .key(format!("filtered-tasks-{}", self.loading)) // recenters when loading
+ .min_width(800)
+ .min_height(600)
+ .max_height("90vh") // max 90% of the screen height
+ .resizable(true)
+ .on_close(props.on_close.clone())
+ .with_child(
+ Mask::new(
+ DataTable::new(filtered_tasks_columns(ctx), self.task_store.clone())
+ .class(FlexFit),
+ )
+ .class(FlexFit)
+ .visible(self.loading),
+ )
+ .into()
+ }
+ }
+}
+
+fn filtered_tasks_columns(
+ ctx: &Context<PdmFilteredTasks>,
+) -> Rc<Vec<DataTableHeader<TaskListItem>>> {
+ Rc::new(vec![
+ DataTableColumn::new(tr!("Remote"))
+ .width("minmax(150px, 1fr)")
+ .get_property_owned(
+ |item: &TaskListItem| match item.upid.parse::<RemoteUpid>() {
+ Ok(upid) => upid.remote().to_string(),
+ Err(_) => String::new(),
+ },
+ )
+ .into(),
+ DataTableColumn::new(tr!("Task"))
+ .flex(2)
+ .get_property_owned(|item: &TaskListItem| {
+ format_optional_remote_upid(&item.upid, false)
+ })
+ .into(),
+ DataTableColumn::new(tr!("Start Time"))
+ .sort_order(false)
+ .width("200px")
+ .get_property_owned(|item: &TaskListItem| render_epoch(item.starttime))
+ .into(),
+ DataTableColumn::new(tr!("Duration"))
+ .sorter(|a: &TaskListItem, b: &TaskListItem| {
+ let duration_a = match a.endtime {
+ Some(endtime) => endtime - a.starttime,
+ None => i64::MAX,
+ };
+ let duration_b = match b.endtime {
+ Some(endtime) => endtime - b.starttime,
+ None => i64::MAX,
+ };
+ duration_a.cmp(&duration_b)
+ })
+ .render(|item: &TaskListItem| {
+ let duration = match item.endtime {
+ Some(endtime) => endtime - item.starttime,
+ None => return String::from("-").into(),
+ };
+ format_duration_human(duration as f64).into()
+ })
+ .into(),
+ DataTableColumn::new(tr!("Status"))
+ .justify("center")
+ .render(|item: &TaskListItem| {
+ let text = item.status.as_deref().unwrap_or("");
+ let icon = match text.into() {
+ TaskStatusClass::Ok => Status::Success,
+ TaskStatusClass::Warning => Status::Warning,
+ TaskStatusClass::Error => Status::Error,
+ };
+ Fa::from(icon).into()
+ })
+ .into(),
+ DataTableColumn::new(tr!("Action"))
+ .justify("center")
+ .render({
+ let link = ctx.link().clone();
+ move |item: &TaskListItem| {
+ let upid = item.upid.clone();
+ let endtime = item.endtime;
+ let link = link.clone();
+ let icon = ActionIcon::new("fa fa-chevron-right").on_activate(move |_| {
+ if let Ok(upid) = upid.parse::<RemoteUpid>() {
+ link.send_message(Msg::ShowTask(Some((upid, endtime))));
+ }
+ });
+ Tooltip::new(icon).tip(tr!("Open Task")).into()
+ }
+ })
+ .into(),
+ ])
+}
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 84efb1b..0cdd26c 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -45,6 +45,8 @@ use guest_panel::GuestPanel;
mod status_row;
use status_row::DashboardStatusRow;
+mod filtered_tasks;
+
/// The default 'max-age' parameter in seconds.
pub const DEFAULT_MAX_AGE_S: u64 = 60;
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-08-26 10:23 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-08-26 10:15 [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 1/8] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 2/8] server: api: remote tasks: add 'remote' filter option Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 3/8] server: api: add remote-tasks statistics Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 4/8] ui: refactor remote upid formatter Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 5/8] ui: tasks: add helper to summarize task categories Dominik Csapak
2025-08-26 10:15 ` Dominik Csapak [this message]
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 7/8] ui: dashboard: add task summaries Dominik Csapak
2025-08-26 10:15 ` [pdm-devel] [PATCH datacenter-manager v4 8/8] ui: dashboard: make task summary time range configurable Dominik Csapak
2025-08-26 12:41 ` [pdm-devel] [PATCH datacenter-manager v4 0/8] add task summary panels in dashboard Stefan Hanreich
2025-09-04 19:21 ` [pdm-devel] applied-series: " Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250826102229.2271453-7-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox