use gloo_net::http::Request; use gloo_timers::callback::Interval; use serde::{Deserialize, Serialize}; use wasm_bindgen_futures::spawn_local; use web_sys::HtmlInputElement; use yew::prelude::*; use yew::{html, Component, Context, Html, TargetCast}; // --- Data Structures (placeholders, to be refined based on backend API) --- #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] pub struct QueueStats { pub current_size: u32, pub color_code: String, // e.g., "green", "yellow", "red" } #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] pub struct TaskSummary { pub hash: String, pub created_at: i64, pub status: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct TaskDetails { pub hash: String, pub created_at: i64, pub status: String, pub script_content: String, pub result: Option, pub error: Option, } // Combined structure for initial fetch #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] pub struct WorkerDataResponse { pub queue_stats: Option, pub tasks: Vec, } // --- Component --- pub enum Msg { UpdateWorkerName(String), FetchData, SetWorkerData(Result), SetQueueStats(Result), ViewTaskDetails(String), // Task hash SetTaskDetails(Result), ClearTaskDetails, IntervalTick, // For interval timer, to trigger queue stats fetch } pub struct App { worker_name_input: String, worker_name_to_monitor: Option, tasks_list: Vec, current_queue_stats: Option, selected_task_details: Option, error_message: Option, is_loading_initial_data: bool, is_loading_task_details: bool, queue_poll_timer: Option, } impl Component for App { type Message = Msg; type Properties = (); fn create(_ctx: &Context) -> Self { Self { worker_name_input: "".to_string(), worker_name_to_monitor: None, tasks_list: Vec::new(), current_queue_stats: None, selected_task_details: None, error_message: None, is_loading_initial_data: false, is_loading_task_details: false, queue_poll_timer: None, } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { Msg::UpdateWorkerName(name) => { self.worker_name_input = name; true } Msg::FetchData => { if self.worker_name_input.trim().is_empty() { self.error_message = Some("Please enter a worker name.".to_string()); return true; } let worker_name = self.worker_name_input.trim().to_string(); self.worker_name_to_monitor = Some(worker_name.clone()); self.error_message = None; self.tasks_list.clear(); self.current_queue_stats = None; self.selected_task_details = None; self.is_loading_initial_data = true; let link = ctx.link().clone(); let tasks_url = format!("/api/worker/{}/tasks_and_stats", worker_name); spawn_local(async move { match Request::get(&tasks_url).send().await { Ok(response) => { if response.ok() { match response.json::().await { Ok(data) => link.send_message(Msg::SetWorkerData(Ok(data))), Err(e) => link.send_message(Msg::SetWorkerData(Err(format!( "Failed to parse worker data: {}", e )))), } } else { link.send_message(Msg::SetWorkerData(Err(format!( "API error: {} {}", response.status(), response.status_text() )))); } } Err(e) => link.send_message(Msg::SetWorkerData(Err(format!( "Network error fetching worker data: {}", e )))), } }); // Set up polling for queue stats let link_for_timer = ctx.link().clone(); let timer = Interval::new(5000, move || { // Poll every 5 seconds link_for_timer.send_message(Msg::IntervalTick); }); if let Some(old_timer) = self.queue_poll_timer.take() { old_timer.cancel(); // Cancel previous timer if any } self.queue_poll_timer = Some(timer); true } Msg::IntervalTick => { if let Some(worker_name) = &self.worker_name_to_monitor { let queue_stats_url = format!("/api/worker/{}/queue_stats", worker_name); let link = ctx.link().clone(); spawn_local(async move { match Request::get(&queue_stats_url).send().await { Ok(response) => { if response.ok() { match response.json::().await { Ok(stats) => { link.send_message(Msg::SetQueueStats(Ok(stats))) } Err(e) => link.send_message(Msg::SetQueueStats(Err( format!("Failed to parse queue stats: {}", e), ))), } } else { link.send_message(Msg::SetQueueStats(Err(format!( "API error (queue_stats): {} {}", response.status(), response.status_text() )))); } } Err(e) => link.send_message(Msg::SetQueueStats(Err(format!( "Network error fetching queue stats: {}", e )))), } }); } false // No direct re-render, SetQueueStats will trigger it } Msg::SetWorkerData(Ok(data)) => { self.tasks_list = data.tasks; self.current_queue_stats = data.queue_stats; self.error_message = None; self.is_loading_initial_data = false; true } Msg::SetWorkerData(Err(err_msg)) => { self.error_message = Some(err_msg); self.is_loading_initial_data = false; if let Some(timer) = self.queue_poll_timer.take() { timer.cancel(); } true } Msg::SetQueueStats(Ok(stats)) => { self.current_queue_stats = Some(stats); // Don't clear main error message here, as this is a background update true } Msg::SetQueueStats(Err(err_msg)) => { log::error!("Failed to update queue stats: {}", err_msg); // Optionally show a non-blocking error for queue stats self.current_queue_stats = None; true } Msg::ViewTaskDetails(hash) => { self.is_loading_task_details = true; self.selected_task_details = None; // Clear previous details let task_details_url = format!("/api/task/{}", hash); let link = ctx.link().clone(); spawn_local(async move { match Request::get(&task_details_url).send().await { Ok(response) => { if response.ok() { match response.json::().await { Ok(details) => { link.send_message(Msg::SetTaskDetails(Ok(details))) } Err(e) => link.send_message(Msg::SetTaskDetails(Err(format!( "Failed to parse task details: {}", e )))), } } else { link.send_message(Msg::SetTaskDetails(Err(format!( "API error (task_details): {} {}", response.status(), response.status_text() )))); } } Err(e) => link.send_message(Msg::SetTaskDetails(Err(format!( "Network error fetching task details: {}", e )))), } }); true } Msg::SetTaskDetails(Ok(details)) => { self.selected_task_details = Some(details); self.error_message = None; // Clear general error if task details load self.is_loading_task_details = false; true } Msg::SetTaskDetails(Err(err_msg)) => { self.error_message = Some(format!("Error loading task details: {}", err_msg)); self.selected_task_details = None; self.is_loading_task_details = false; true } Msg::ClearTaskDetails => { self.selected_task_details = None; true } } } fn view(&self, ctx: &Context) -> Html { let link = ctx.link(); let on_worker_name_input = link.callback(|e: InputEvent| { let input: HtmlInputElement = e.target_unchecked_into(); Msg::UpdateWorkerName(input.value()) }); html! {

{ "Rhai Worker Monitor" }

().value()) } })} />
if let Some(err) = &self.error_message {

{ err }

} if self.worker_name_to_monitor.is_some() && !self.is_loading_initial_data && self.error_message.is_none() {

{ format!("Monitoring: {}", self.worker_name_to_monitor.as_ref().unwrap()) }

{ "Queue Status" }

{ if let Some(stats) = &self.current_queue_stats { // TODO: Implement actual color coding and bar visualization html! {

{format!("Tasks in queue: {} ({})", stats.current_size, stats.color_code)}

} } else { html! {

{ "Loading queue stats..." }

} } }

{ "Tasks" }

{ self.view_tasks_table(ctx) } { self.view_selected_task_details(ctx) } } else if self.is_loading_initial_data {

{ "Loading worker data..." }

}
} } } impl App { fn view_tasks_table(&self, ctx: &Context) -> Html { if self.tasks_list.is_empty() && self.worker_name_to_monitor.is_some() && !self.is_loading_initial_data { return html! {

{ "No tasks found for this worker, or worker not found." }

}; } if !self.tasks_list.is_empty() { html! { { for self.tasks_list.iter().map(|task| self.view_task_row(ctx, task)) }
{ "Hash (click to view)" } { "Created At (UTC)" } { "Status" }
} } else { html! {} } } fn view_task_row(&self, ctx: &Context, task: &TaskSummary) -> Html { let task_hash_clone = task.hash.clone(); let created_at_str = chrono::DateTime::from_timestamp(task.created_at, 0).map_or_else( || "Invalid date".to_string(), |dt| dt.format("%Y-%m-%d %H:%M:%S").to_string(), ); html! { { task.hash.chars().take(12).collect::() }{ "..." } { created_at_str } { &task.status } } } fn view_selected_task_details(&self, ctx: &Context) -> Html { if self.is_loading_task_details { return html! {

{ "Loading task details..." }

}; } if let Some(details) = &self.selected_task_details { let created_at_str = chrono::DateTime::from_timestamp(details.created_at, 0) .map_or_else( || "Invalid date".to_string(), |dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(), ); html! {

{ format!("Task Details: {}", details.hash) }

{ "Created At: " }{ created_at_str }

{ "Status: " }{ &details.status }

{ "Script Content:" }

{ &details.script_content }
if let Some(result) = &details.result {

{ "Result:" }

{ result }
} if let Some(error) = &details.error {

{ "Error:" }

{ error }
}
} } else { html! {} } } }