From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 15E2D1FF16B for ; Tue, 26 Aug 2025 12:23:00 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7611A2F264; Tue, 26 Aug 2025 12:23:05 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Date: Tue, 26 Aug 2025 12:15:16 +0200 Message-ID: <20250826102229.2271453-7-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.2 In-Reply-To: <20250826102229.2271453-1-d.csapak@proxmox.com> References: <20250826102229.2271453-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.022 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH datacenter-manager v4 6/8] ui: add dialog to show filtered tasks X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "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 --- 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, + #[serde(skip_serializing_if = "Option::is_none")] pub since: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub until: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub typefilter: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub statusfilter: Option>, } 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>, +} + +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 for VNode { + fn from(val: FilteredTasks) -> Self { + let comp = VComp::new::(Rc::new(val), None); + VNode::from(comp) + } +} + +pub enum Msg { + LoadFinished(Result, Error>), + ShowTask(Option<(RemoteUpid, Option)>), +} + +pub struct PdmFilteredTasks { + task_store: Store, + task_info: Option<(RemoteUpid, Option)>, + loading: bool, + last_error: Option, + _async_pool: AsyncPool, +} + +impl PdmFilteredTasks { + async fn load( + since: i64, + status: TaskStatusClass, + grouping: TaskWorkerType, + ) -> Result, 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 { + 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, 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) -> 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, +) -> Rc>> { + Rc::new(vec![ + DataTableColumn::new(tr!("Remote")) + .width("minmax(150px, 1fr)") + .get_property_owned( + |item: &TaskListItem| match item.upid.parse::() { + 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::() { + 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