From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager v2 6/7] ui: add dialog to show filtered tasks
Date: Wed, 19 Feb 2025 13:28:23 +0100 [thread overview]
Message-ID: <20250219122824.2043990-7-d.csapak@proxmox.com> (raw)
In-Reply-To: <20250219122824.2043990-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>
---
lib/pdm-api-types/src/lib.rs | 5 +
ui/src/dashboard/filtered_tasks.rs | 297 +++++++++++++++++++++++++++++
ui/src/dashboard/mod.rs | 2 +
3 files changed, 304 insertions(+)
create mode 100644 ui/src/dashboard/filtered_tasks.rs
diff --git a/lib/pdm-api-types/src/lib.rs b/lib/pdm-api-types/src/lib.rs
index 47e5894..ccf1d43 100644
--- a/lib/pdm-api-types/src/lib.rs
+++ b/lib/pdm-api-types/src/lib.rs
@@ -546,9 +546,14 @@ pub struct TaskFilters {
pub errors: bool,
#[serde(default)]
pub running: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub userfilter: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub since: Option<i64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub until: Option<i64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub typefilter: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub statusfilter: Option<Vec<TaskStateType>>,
}
diff --git a/ui/src/dashboard/filtered_tasks.rs b/ui/src/dashboard/filtered_tasks.rs
new file mode 100644
index 0000000..c8cbf34
--- /dev/null
+++ b/ui/src/dashboard/filtered_tasks.rs
@@ -0,0 +1,297 @@
+use std::rc::Rc;
+
+use anyhow::Error;
+use proxmox_yew_comp::{
+ common_api_types::{TaskListItem, TaskStatusClass},
+ http_get,
+ utils::{format_duration_human, render_epoch},
+ Status, TaskViewer,
+};
+use pwt_macros::builder;
+use yew::{
+ html::IntoEventCallback,
+ virtual_dom::{VComp, VNode},
+ Component, Properties,
+};
+
+use pwt::{
+ css::FlexFit,
+ prelude::*,
+ widget::{
+ data_table::{DataTable, DataTableColumn, DataTableHeader},
+ ActionIcon, AlertDialog, Mask, Tooltip,
+ },
+ AsyncPool,
+};
+use pwt::{state::Store, tr, widget::Dialog};
+
+use pdm_api_types::{RemoteUpid, TaskFilters, TaskStateType};
+
+use crate::tasks::{format_optional_remote_upid, get_type_title, map_worker_type};
+
+#[derive(PartialEq, Clone)]
+pub enum TaskGroup {
+ Remote(String), // remote name
+ Type(String), // worker type
+}
+
+#[derive(PartialEq, Properties)]
+#[builder]
+pub struct FilteredTasks {
+ grouping: TaskGroup,
+ task_status: TaskStatusClass,
+ since: i64,
+
+ #[prop_or_default]
+ #[builder_cb(IntoEventCallback, into_event_callback, ())]
+ /// Callback for closing the Dialog
+ on_close: Option<Callback<()>>,
+}
+
+impl FilteredTasks {
+ /// Create new instance with filters for task type and status, beginning from 'since'
+ pub fn new(since: i64, grouping: TaskGroup, task_status: TaskStatusClass) -> Self {
+ yew::props!(Self {
+ since,
+ grouping,
+ task_status,
+ })
+ }
+}
+
+impl From<FilteredTasks> for VNode {
+ fn from(val: FilteredTasks) -> Self {
+ let comp = VComp::new::<PdmFilteredTasks>(Rc::new(val), None);
+ VNode::from(comp)
+ }
+}
+
+pub enum Msg {
+ LoadFinished(Result<Vec<TaskListItem>, Error>),
+ ShowTask(Option<(RemoteUpid, Option<i64>)>),
+}
+
+pub struct PdmFilteredTasks {
+ task_store: Store<TaskListItem>,
+ task_info: Option<(RemoteUpid, Option<i64>)>,
+ loading: bool,
+ last_error: Option<Error>,
+ _async_pool: AsyncPool,
+}
+
+impl PdmFilteredTasks {
+ async fn load(
+ since: i64,
+ status: TaskStatusClass,
+ grouping: TaskGroup,
+ ) -> Result<Vec<TaskListItem>, Error> {
+ // TODO replace with pdm client call
+ let status = match status {
+ TaskStatusClass::Ok => TaskStateType::OK,
+ TaskStatusClass::Warning => TaskStateType::Warning,
+ TaskStatusClass::Error => TaskStateType::Error,
+ };
+ let mut filters = TaskFilters {
+ since: Some(since),
+ limit: 0,
+ userfilter: None,
+ until: None,
+ typefilter: None,
+ statusfilter: Some(vec![status.clone()]),
+
+ start: 0,
+ errors: false,
+ running: false,
+ };
+
+ if let TaskGroup::Type(worker_type) = &grouping {
+ filters.typefilter = Some(worker_type.to_string());
+ }
+
+ let mut params = serde_json::to_value(filters)?;
+
+ if let TaskGroup::Remote(remote) = grouping {
+ params["remote"] = serde_json::Value::String(remote);
+ }
+
+ http_get("/remote-tasks/list", Some(params)).await
+ }
+}
+
+impl Component for PdmFilteredTasks {
+ type Message = Msg;
+ type Properties = FilteredTasks;
+
+ fn create(ctx: &Context<Self>) -> Self {
+ let props = ctx.props();
+ let since = props.since;
+ let grouping = props.grouping.clone();
+ let status = props.task_status;
+ let link = ctx.link().clone();
+ let _async_pool = AsyncPool::new();
+ _async_pool.send_future(link, async move {
+ let res = Self::load(since, status, grouping).await;
+ Msg::LoadFinished(res)
+ });
+ Self {
+ task_store: Store::new(),
+ task_info: None,
+ loading: true,
+ last_error: None,
+ _async_pool,
+ }
+ }
+
+ fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+ match msg {
+ Msg::LoadFinished(Ok(task_list_items)) => {
+ self.last_error = None;
+ self.loading = false;
+ self.task_store.set_data(task_list_items);
+ match _ctx.props().grouping.clone() {
+ TaskGroup::Remote(_) => {}
+ TaskGroup::Type(worker_type) => {
+ self.task_store.set_filter(move |entry: &TaskListItem| {
+ worker_type == map_worker_type(&entry.worker_type)
+ });
+ }
+ }
+ }
+ Msg::LoadFinished(Err(err)) => {
+ self.loading = false;
+ self.last_error = Some(err);
+ }
+ Msg::ShowTask(task) => {
+ self.task_info = task;
+ }
+ }
+ true
+ }
+
+ fn view(&self, ctx: &Context<Self>) -> Html {
+ let props = ctx.props();
+ if let Some(err) = &self.last_error {
+ return AlertDialog::new(err.to_string())
+ .on_close(ctx.props().on_close.clone())
+ .into();
+ }
+
+ if let Some((upid, endtime)) = &self.task_info {
+ // TODO PBS
+ let base_url = format!("/pve/remotes/{}/tasks", upid.remote());
+ TaskViewer::new(upid.to_string())
+ .endtime(endtime)
+ .base_url(base_url)
+ .on_close({
+ let link = ctx.link().clone();
+ move |_| link.send_message(Msg::ShowTask(None))
+ })
+ .into()
+ } else {
+ let title = format!(
+ "{} - {}",
+ match &props.grouping {
+ TaskGroup::Remote(remote) => remote.to_string(),
+ TaskGroup::Type(worker_type) => get_type_title(worker_type),
+ },
+ match props.task_status {
+ TaskStatusClass::Ok => tr!("OK"),
+ TaskStatusClass::Warning => tr!("Warning"),
+ TaskStatusClass::Error => tr!("Error"),
+ },
+ );
+ Dialog::new(title)
+ .key(format!("filtered-tasks-{}", self.loading)) // recenters when loading
+ .min_width(800)
+ .min_height(600)
+ .max_height("90vh") // max 90% of the screen height
+ .resizable(true)
+ .on_close(props.on_close.clone())
+ .with_child(
+ Mask::new(
+ DataTable::new(filtered_tasks_columns(ctx), self.task_store.clone())
+ .class(FlexFit),
+ )
+ .class(FlexFit)
+ .visible(self.loading),
+ )
+ .into()
+ }
+ }
+}
+
+fn filtered_tasks_columns(
+ ctx: &Context<PdmFilteredTasks>,
+) -> Rc<Vec<DataTableHeader<TaskListItem>>> {
+ Rc::new(vec![
+ DataTableColumn::new(tr!("Remote"))
+ .width("minmax(150px, 1fr)")
+ .get_property_owned(
+ |item: &TaskListItem| match item.upid.parse::<RemoteUpid>() {
+ Ok(upid) => upid.remote().to_string(),
+ Err(_) => String::new(),
+ },
+ )
+ .into(),
+ DataTableColumn::new(tr!("Task"))
+ .flex(2)
+ .get_property_owned(|item: &TaskListItem| {
+ format_optional_remote_upid(&item.upid, false)
+ })
+ .into(),
+ DataTableColumn::new(tr!("Start Time"))
+ .sort_order(false)
+ .width("200px")
+ .get_property_owned(|item: &TaskListItem| render_epoch(item.starttime))
+ .into(),
+ DataTableColumn::new(tr!("Duration"))
+ .sorter(|a: &TaskListItem, b: &TaskListItem| {
+ let duration_a = match a.endtime {
+ Some(endtime) => endtime - a.starttime,
+ None => i64::MAX,
+ };
+ let duration_b = match b.endtime {
+ Some(endtime) => endtime - b.starttime,
+ None => i64::MAX,
+ };
+ duration_a.cmp(&duration_b)
+ })
+ .render(|item: &TaskListItem| {
+ let duration = match item.endtime {
+ Some(endtime) => endtime - item.starttime,
+ None => return String::from("-").into(),
+ };
+ format_duration_human(duration as f64).into()
+ })
+ .into(),
+ DataTableColumn::new(tr!("Status"))
+ .justify("center")
+ .render(|item: &TaskListItem| {
+ let text = item.status.as_deref().unwrap_or("");
+ let icon = match text.into() {
+ TaskStatusClass::Ok => Status::Success,
+ TaskStatusClass::Warning => Status::Warning,
+ TaskStatusClass::Error => Status::Error,
+ };
+ icon.to_fa_icon().into()
+ })
+ .into(),
+ DataTableColumn::new(tr!("Action"))
+ .justify("center")
+ .render({
+ let link = ctx.link().clone();
+ move |item: &TaskListItem| {
+ let upid = item.upid.clone();
+ let endtime = item.endtime;
+ let link = link.clone();
+ let icon = ActionIcon::new("fa fa-chevron-right").on_activate(move |_| {
+ if let Ok(upid) = upid.parse::<RemoteUpid>() {
+ link.send_message(Msg::ShowTask(Some((upid, endtime))));
+ }
+ });
+ Tooltip::new(icon).tip(tr!("Open Task")).into()
+ }
+ })
+ .into(),
+ ])
+}
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index ea0cf5e..9d79cd3 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -26,6 +26,8 @@ pub use top_entities::TopEntities;
mod subscription_info;
pub use subscription_info::SubscriptionInfo;
+mod filtered_tasks;
+
#[derive(Properties, PartialEq)]
pub struct Dashboard {
#[prop_or(60)]
--
2.39.5
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
next prev parent reply other threads:[~2025-02-19 12:28 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-02-19 12:28 [pdm-devel] [PATCH datacenter-manager v2 0/7] add task summary panels in dashboard Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 1/7] server: task cache: treat a limit of 0 as unbounded Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 2/7] server: api: remote tasks: add 'remote' filter option Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 3/7] server: api: add remote-tasks statistics Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 4/7] ui: refactor remote upid formatter Dominik Csapak
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 5/7] ui: tasks: add helper to summarize task categories Dominik Csapak
2025-02-19 12:28 ` Dominik Csapak [this message]
2025-02-19 12:28 ` [pdm-devel] [PATCH datacenter-manager v2 7/7] ui: dashboard: add task summaries Dominik Csapak
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=20250219122824.2043990-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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal