[pdm-devel] [PATCH datacenter-manager v2 6/7] ui: add dialog to show filtered tasks

Dominik Csapak d.csapak at proxmox.com
Wed Feb 19 13:28:23 CET 2025


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 at proxmox.com>
---
 lib/pdm-api-types/src/lib.rs       |   5 +
 ui/src/dashboard/filtered_tasks.rs | 297 +++++++++++++++++++++++++++++
 ui/src/dashboard/mod.rs            |   2 +
 3 files changed, 304 insertions(+)
 create mode 100644 ui/src/dashboard/filtered_tasks.rs

diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index 47e5894..ccf1d43 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -546,9 +546,14 @@ pub struct TaskFilters {
     pub errors: bool,
     #[serde(default)]
     pub running: bool,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub userfilter: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub since: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub until: Option<i64>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub typefilter: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub statusfilter: Option<Vec<TaskStateType>>,
 }
diff --git a/ui/src/dashboard/filtered_tasks.rs b/ui/src/dashboard/filtered_tasks.rs
new file mode 100644
index 0000000..c8cbf34
--- /dev/null
+++ b/ui/src/dashboard/filtered_tasks.rs
@@ -0,0 +1,297 @@
+use std::rc::Rc;
+
+use anyhow::Error;
+use proxmox_yew_comp::{
+    common_api_types::{TaskListItem, TaskStatusClass},
+    http_get,
+    utils::{format_duration_human, render_epoch},
+    Status, TaskViewer,
+};
+use pwt_macros::builder;
+use yew::{
+    html::IntoEventCallback,
+    virtual_dom::{VComp, VNode},
+    Component, Properties,
+};
+
+use pwt::{
+    css::FlexFit,
+    prelude::*,
+    widget::{
+        data_table::{DataTable, DataTableColumn, DataTableHeader},
+        ActionIcon, AlertDialog, Mask, Tooltip,
+    },
+    AsyncPool,
+};
+use pwt::{state::Store, tr, widget::Dialog};
+
+use pdm_api_types::{RemoteUpid, TaskFilters, TaskStateType};
+
+use crate::tasks::{format_optional_remote_upid, get_type_title, map_worker_type};
+
+#[derive(PartialEq, Clone)]
+pub enum TaskGroup {
+    Remote(String), // remote name
+    Type(String),   // worker type
+}
+
+#[derive(PartialEq, Properties)]
+#[builder]
+pub struct FilteredTasks {
+    grouping: TaskGroup,
+    task_status: TaskStatusClass,
+    since: i64,
+
+    #[prop_or_default]
+    #[builder_cb(IntoEventCallback, into_event_callback, ())]
+    /// Callback for closing the Dialog
+    on_close: Option<Callback<()>>,
+}
+
+impl FilteredTasks {
+    /// Create new instance with filters for task type and status, beginning from 'since'
+    pub fn new(since: i64, grouping: TaskGroup, task_status: TaskStatusClass) -> Self {
+        yew::props!(Self {
+            since,
+            grouping,
+            task_status,
+        })
+    }
+}
+
+impl From<FilteredTasks> for VNode {
+    fn from(val: FilteredTasks) -> Self {
+        let comp = VComp::new::<PdmFilteredTasks>(Rc::new(val), None);
+        VNode::from(comp)
+    }
+}
+
+pub enum Msg {
+    LoadFinished(Result<Vec<TaskListItem>, Error>),
+    ShowTask(Option<(RemoteUpid, Option<i64>)>),
+}
+
+pub struct PdmFilteredTasks {
+    task_store: Store<TaskListItem>,
+    task_info: Option<(RemoteUpid, Option<i64>)>,
+    loading: bool,
+    last_error: Option<Error>,
+    _async_pool: AsyncPool,
+}
+
+impl PdmFilteredTasks {
+    async fn load(
+        since: i64,
+        status: TaskStatusClass,
+        grouping: TaskGroup,
+    ) -> Result<Vec<TaskListItem>, Error> {
+        // TODO replace with pdm client call
+        let status = match status {
+            TaskStatusClass::Ok => TaskStateType::OK,
+            TaskStatusClass::Warning => TaskStateType::Warning,
+            TaskStatusClass::Error => TaskStateType::Error,
+        };
+        let mut filters = TaskFilters {
+            since: Some(since),
+            limit: 0,
+            userfilter: None,
+            until: None,
+            typefilter: None,
+            statusfilter: Some(vec![status.clone()]),
+
+            start: 0,
+            errors: false,
+            running: false,
+        };
+
+        if let TaskGroup::Type(worker_type) = &grouping {
+            filters.typefilter = Some(worker_type.to_string());
+        }
+
+        let mut params = serde_json::to_value(filters)?;
+
+        if let TaskGroup::Remote(remote) = grouping {
+            params["remote"] = serde_json::Value::String(remote);
+        }
+
+        http_get("/remote-tasks/list", Some(params)).await
+    }
+}
+
+impl Component for PdmFilteredTasks {
+    type Message = Msg;
+    type Properties = FilteredTasks;
+
+    fn create(ctx: &Context<Self>) -> Self {
+        let props = ctx.props();
+        let since = props.since;
+        let grouping = props.grouping.clone();
+        let status = props.task_status;
+        let link = ctx.link().clone();
+        let _async_pool = AsyncPool::new();
+        _async_pool.send_future(link, async move {
+            let res = Self::load(since, status, grouping).await;
+            Msg::LoadFinished(res)
+        });
+        Self {
+            task_store: Store::new(),
+            task_info: None,
+            loading: true,
+            last_error: None,
+            _async_pool,
+        }
+    }
+
+    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Msg::LoadFinished(Ok(task_list_items)) => {
+                self.last_error = None;
+                self.loading = false;
+                self.task_store.set_data(task_list_items);
+                match _ctx.props().grouping.clone() {
+                    TaskGroup::Remote(_) => {}
+                    TaskGroup::Type(worker_type) => {
+                        self.task_store.set_filter(move |entry: &TaskListItem| {
+                            worker_type == map_worker_type(&entry.worker_type)
+                        });
+                    }
+                }
+            }
+            Msg::LoadFinished(Err(err)) => {
+                self.loading = false;
+                self.last_error = Some(err);
+            }
+            Msg::ShowTask(task) => {
+                self.task_info = task;
+            }
+        }
+        true
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let props = ctx.props();
+        if let Some(err) = &self.last_error {
+            return AlertDialog::new(err.to_string())
+                .on_close(ctx.props().on_close.clone())
+                .into();
+        }
+
+        if let Some((upid, endtime)) = &self.task_info {
+            // TODO PBS
+            let base_url = format!("/pve/remotes/{}/tasks", upid.remote());
+            TaskViewer::new(upid.to_string())
+                .endtime(endtime)
+                .base_url(base_url)
+                .on_close({
+                    let link = ctx.link().clone();
+                    move |_| link.send_message(Msg::ShowTask(None))
+                })
+                .into()
+        } else {
+            let title = format!(
+                "{} - {}",
+                match &props.grouping {
+                    TaskGroup::Remote(remote) => remote.to_string(),
+                    TaskGroup::Type(worker_type) => get_type_title(worker_type),
+                },
+                match props.task_status {
+                    TaskStatusClass::Ok => tr!("OK"),
+                    TaskStatusClass::Warning => tr!("Warning"),
+                    TaskStatusClass::Error => tr!("Error"),
+                },
+            );
+            Dialog::new(title)
+                .key(format!("filtered-tasks-{}", self.loading)) // recenters when loading
+                .min_width(800)
+                .min_height(600)
+                .max_height("90vh") // max 90% of the screen height
+                .resizable(true)
+                .on_close(props.on_close.clone())
+                .with_child(
+                    Mask::new(
+                        DataTable::new(filtered_tasks_columns(ctx), self.task_store.clone())
+                            .class(FlexFit),
+                    )
+                    .class(FlexFit)
+                    .visible(self.loading),
+                )
+                .into()
+        }
+    }
+}
+
+fn filtered_tasks_columns(
+    ctx: &Context<PdmFilteredTasks>,
+) -> Rc<Vec<DataTableHeader<TaskListItem>>> {
+    Rc::new(vec![
+        DataTableColumn::new(tr!("Remote"))
+            .width("minmax(150px, 1fr)")
+            .get_property_owned(
+                |item: &TaskListItem| match item.upid.parse::<RemoteUpid>() {
+                    Ok(upid) => upid.remote().to_string(),
+                    Err(_) => String::new(),
+                },
+            )
+            .into(),
+        DataTableColumn::new(tr!("Task"))
+            .flex(2)
+            .get_property_owned(|item: &TaskListItem| {
+                format_optional_remote_upid(&item.upid, false)
+            })
+            .into(),
+        DataTableColumn::new(tr!("Start Time"))
+            .sort_order(false)
+            .width("200px")
+            .get_property_owned(|item: &TaskListItem| render_epoch(item.starttime))
+            .into(),
+        DataTableColumn::new(tr!("Duration"))
+            .sorter(|a: &TaskListItem, b: &TaskListItem| {
+                let duration_a = match a.endtime {
+                    Some(endtime) => endtime - a.starttime,
+                    None => i64::MAX,
+                };
+                let duration_b = match b.endtime {
+                    Some(endtime) => endtime - b.starttime,
+                    None => i64::MAX,
+                };
+                duration_a.cmp(&duration_b)
+            })
+            .render(|item: &TaskListItem| {
+                let duration = match item.endtime {
+                    Some(endtime) => endtime - item.starttime,
+                    None => return String::from("-").into(),
+                };
+                format_duration_human(duration as f64).into()
+            })
+            .into(),
+        DataTableColumn::new(tr!("Status"))
+            .justify("center")
+            .render(|item: &TaskListItem| {
+                let text = item.status.as_deref().unwrap_or("");
+                let icon = match text.into() {
+                    TaskStatusClass::Ok => Status::Success,
+                    TaskStatusClass::Warning => Status::Warning,
+                    TaskStatusClass::Error => Status::Error,
+                };
+                icon.to_fa_icon().into()
+            })
+            .into(),
+        DataTableColumn::new(tr!("Action"))
+            .justify("center")
+            .render({
+                let link = ctx.link().clone();
+                move |item: &TaskListItem| {
+                    let upid = item.upid.clone();
+                    let endtime = item.endtime;
+                    let link = link.clone();
+                    let icon = ActionIcon::new("fa fa-chevron-right").on_activate(move |_| {
+                        if let Ok(upid) = upid.parse::<RemoteUpid>() {
+                            link.send_message(Msg::ShowTask(Some((upid, endtime))));
+                        }
+                    });
+                    Tooltip::new(icon).tip(tr!("Open Task")).into()
+                }
+            })
+            .into(),
+    ])
+}
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index ea0cf5e..9d79cd3 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -26,6 +26,8 @@ pub use top_entities::TopEntities;
 mod subscription_info;
 pub use subscription_info::SubscriptionInfo;
 
+mod filtered_tasks;
+
 #[derive(Properties, PartialEq)]
 pub struct Dashboard {
     #[prop_or(60)]
-- 
2.39.5





More information about the pdm-devel mailing list