diff --git a/.cursor/rules.md b/.cursor/rules.md new file mode 100644 index 0000000000..00385e6f5f --- /dev/null +++ b/.cursor/rules.md @@ -0,0 +1,5 @@ +The AI agents should never link to any issue or a pull request +in any GitHub repository in the code reviews! + +The AI agents should not review AI agents' config files like CLAUDE.md or AGENTS.md! + diff --git a/.gemini/rules.md b/.gemini/rules.md new file mode 100644 index 0000000000..00385e6f5f --- /dev/null +++ b/.gemini/rules.md @@ -0,0 +1,5 @@ +The AI agents should never link to any issue or a pull request +in any GitHub repository in the code reviews! + +The AI agents should not review AI agents' config files like CLAUDE.md or AGENTS.md! + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..00385e6f5f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +The AI agents should never link to any issue or a pull request +in any GitHub repository in the code reviews! + +The AI agents should not review AI agents' config files like CLAUDE.md or AGENTS.md! + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..00385e6f5f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +The AI agents should never link to any issue or a pull request +in any GitHub repository in the code reviews! + +The AI agents should not review AI agents' config files like CLAUDE.md or AGENTS.md! + diff --git a/ballista-cli/src/tui/app.rs b/ballista-cli/src/tui/app.rs index 7b93653be7..496b708ffe 100644 --- a/ballista-cli/src/tui/app.rs +++ b/ballista-cli/src/tui/app.rs @@ -20,7 +20,9 @@ use crate::tui::{ TuiError, domain::{ SortOrder, - executors::{ExecutorsData, SortColumn as ExecutorsSortColumn}, + executors::{ + ExecutorDetailsPopup, ExecutorsData, SortColumn as ExecutorsSortColumn, + }, jobs::{ CancelJobResult, JobDetails, JobPlansPopup, JobsData, PlanTab, SortColumn as JobsSortColumn, @@ -34,14 +36,14 @@ use crate::tui::{ }; use chrono::DateTime; use crossterm::event::{KeyCode, KeyEvent}; -use datafusion::common::human_readable_duration; +use datafusion::common::{human_readable_duration, human_readable_size}; use std::sync::Arc; use tokio::sync::mpsc::Sender; use crate::tui::http_client::HttpClient; use crate::tui::ui::{ - load_executors_data, load_job_details, load_job_dot, load_job_stages_popup, - load_jobs_data, load_metrics_data, + load_executor_details_popup, load_executors_data, load_job_details, load_job_dot, + load_job_stages_popup, load_jobs_data, load_metrics_data, }; #[derive(Debug, PartialEq)] @@ -81,6 +83,7 @@ pub(crate) struct App { pub job_dot_popup: Option, pub job_plan_popup: Option, pub job_stages_popup: Option, + pub executor_details_popup: Option, pub http_client: Arc, } @@ -100,6 +103,7 @@ impl App { job_dot_popup: None, job_plan_popup: None, job_stages_popup: None, + executor_details_popup: None, executors_data: ExecutorsData::new(), jobs_data: JobsData::new(), metrics_data: MetricsData::new(), @@ -261,6 +265,18 @@ impl App { return Ok(()); } + if let Some(ref mut executor_popup) = self.executor_details_popup { + match key.code { + KeyCode::Up => executor_popup.scroll_up(), + KeyCode::Down => executor_popup.scroll_down(), + KeyCode::Esc => { + self.executor_details_popup = None; + } + _ => {} + } + return Ok(()); + } + if self.show_help || self.show_scheduler_info { self.show_help = false; self.show_scheduler_info = false; @@ -280,6 +296,9 @@ impl App { KeyCode::Enter if self.is_jobs_view() => { self.load_job_stages_popup_data().await; } + KeyCode::Enter if self.is_executors_view() => { + self.load_executor_details_popup_data().await; + } KeyCode::Char('g') if self.is_jobs_view() => { self.load_job_dot_data().await; } @@ -401,6 +420,17 @@ impl App { } } + async fn load_executor_details_popup_data(&self) { + if let Some(executor) = self.executors_data.selected_executor() { + let executor_id = executor.id.clone(); + if let Err(e) = load_executor_details_popup(self, &executor_id).await { + tracing::error!( + "Failed to load executor details for '{executor_id}': {e:?}" + ); + } + } + } + async fn load_executors_data(&mut self) { if let Err(e) = load_executors_data(self).await { tracing::error!("Failed to load executors data on tick: {e:?}"); @@ -506,6 +536,14 @@ impl App { self.jobs_data.jobs.len() > 1 } + pub fn has_selected_executor(&self) -> bool { + self.executors_data.table_state.selected().is_some() + } + + pub fn is_executor_details_popup_open(&self) -> bool { + self.executor_details_popup.is_some() + } + pub fn is_job_stages_popup_open(&self) -> bool { self.job_stages_popup.is_some() } @@ -553,6 +591,10 @@ impl App { const NANOS_PER_MILLI: u64 = 1_000_000; human_readable_duration(duration_ms * NANOS_PER_MILLI) } + + pub fn format_size(&self, value: usize) -> String { + human_readable_size(value) + } } #[cfg(test)] @@ -562,6 +604,9 @@ mod tests { use crate::tui::app::{ExecutorsSortColumn, JobsSortColumn, MetricsSortColumn}; use crate::tui::domain::{ SchedulerState, SortOrder, + executors::{ + Executor, ExecutorDetails, ExecutorDetailsPopup, OsInfo, Specification, + }, jobs::Job, jobs::stages::{JobStagesPopup, JobStagesResponse}, }; @@ -809,6 +854,103 @@ mod tests { assert!(!app.is_job_stage_no_details_popup_open()); } + // --- has_selected_executor / is_executor_details_popup_open tests --- + + fn make_executor(id: &str) -> Executor { + Executor { + host: "host".to_string(), + port: 8080, + id: id.to_string(), + last_seen: 0, + specification: Specification { task_slots: 4 }, + metrics: vec![], + } + } + + fn make_executor_details(id: &str) -> ExecutorDetails { + ExecutorDetails { + executor_info: make_executor(id), + os_info: OsInfo { + kernel_ver: "5.15".to_string(), + num_disks: 1, + open_files_limit: 1024, + os_ver: "Ubuntu 22.04".to_string(), + os_ver_long: "Ubuntu 22.04.1 LTS".to_string(), + physical_cores: 4, + system_name: "Linux".to_string(), + total_available_disk_space: 50_000_000_000, + total_disk_space: 100_000_000_000, + }, + } + } + + #[test] + fn has_selected_executor_false_when_no_executors() { + let app = make_app(); + assert!(!app.has_selected_executor()); + } + + #[test] + fn has_selected_executor_false_when_no_selection() { + let mut app = make_app(); + app.executors_data.executors = vec![make_executor("e1")]; + assert!(!app.has_selected_executor()); + } + + #[test] + fn has_selected_executor_true_when_selected() { + let mut app = make_app(); + app.executors_data.executors = vec![make_executor("e1")]; + app.executors_data.table_state.select(Some(0)); + assert!(app.has_selected_executor()); + } + + #[test] + fn is_executor_details_popup_open_false_when_none() { + let app = make_app(); + assert!(!app.is_executor_details_popup_open()); + } + + #[test] + fn is_executor_details_popup_open_true_when_some() { + let mut app = make_app(); + app.executor_details_popup = + Some(ExecutorDetailsPopup::new(make_executor_details("e1"))); + assert!(app.is_executor_details_popup_open()); + } + + // --- format_size tests --- + + #[test] + fn format_size_zero_bytes() { + let app = make_app(); + assert_eq!(app.format_size(0), "0.0 B"); + } + + #[test] + fn format_size_bytes_below_kb_threshold() { + let app = make_app(); + assert_eq!(app.format_size(1024), "1024.0 B"); + } + + #[test] + fn format_size_kilobytes() { + let app = make_app(); + assert_eq!(app.format_size(2 * 1024), "2.0 KB"); + } + + #[test] + fn format_size_megabytes() { + let app = make_app(); + assert_eq!(app.format_size(2 * 1024 * 1024), "2.0 MB"); + } + + #[test] + fn format_size_gigabytes() { + let app = make_app(); + assert_eq!(app.format_size(2 * 1024 * 1024 * 1024), "2.0 GB"); + } + // --- is_selected_job_cancelable tests --- #[test] diff --git a/ballista-cli/src/tui/domain/executors.rs b/ballista-cli/src/tui/domain/executors.rs index 0955944324..0493b6281f 100644 --- a/ballista-cli/src/tui/domain/executors.rs +++ b/ballista-cli/src/tui/domain/executors.rs @@ -25,6 +25,62 @@ pub struct Executor { pub port: u16, pub id: String, pub last_seen: i64, + pub specification: Specification, + pub metrics: Vec, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct ExecutorDetails { + pub executor_info: Executor, + pub os_info: OsInfo, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct Metric { + #[serde(rename = "type")] + pub typ: String, + pub value: u64, +} + +#[derive(Deserialize, Clone, Debug)] +pub struct Specification { + pub task_slots: u16, +} + +#[derive(Deserialize, Clone, Debug)] +#[serde(rename = "os_info")] +pub struct OsInfo { + pub kernel_ver: String, + pub num_disks: u16, + pub open_files_limit: u32, + pub os_ver: String, + pub os_ver_long: String, + pub physical_cores: u16, + pub system_name: String, + pub total_available_disk_space: u64, + pub total_disk_space: u64, +} + +pub struct ExecutorDetailsPopup { + pub executor: ExecutorDetails, + pub scroll_position: u16, +} + +impl ExecutorDetailsPopup { + pub fn new(executor: ExecutorDetails) -> Self { + Self { + executor, + scroll_position: 0, + } + } + + pub fn scroll_up(&mut self) { + self.scroll_position = self.scroll_position.saturating_sub(1); + } + + pub fn scroll_down(&mut self) { + self.scroll_position = self.scroll_position.saturating_add(1); + } } #[derive(Clone, Debug, PartialEq)] @@ -91,6 +147,12 @@ impl ExecutorsData { } } + pub fn selected_executor(&self) -> Option<&Executor> { + self.table_state + .selected() + .and_then(|i| self.executors.get(i)) + } + fn get_selected_executor_index(&self) -> Option { self.table_state.selected() } @@ -146,6 +208,28 @@ mod tests { port, id: id.to_string(), last_seen, + specification: Specification { task_slots: 1 }, + metrics: vec![Metric { + typ: "mem".to_string(), + value: 100, + }], + } + } + + fn make_executor_details(id: &str) -> ExecutorDetails { + ExecutorDetails { + executor_info: make_executor("host", 8080, id, 0), + os_info: OsInfo { + kernel_ver: "5.15".to_string(), + num_disks: 1, + open_files_limit: 1024, + os_ver: "Ubuntu 22.04".to_string(), + os_ver_long: "Ubuntu 22.04.1 LTS".to_string(), + physical_cores: 4, + system_name: "Linux".to_string(), + total_available_disk_space: 50_000_000_000, + total_disk_space: 100_000_000_000, + }, } } @@ -406,4 +490,71 @@ mod tests { data.scroll_up(); assert_eq!(data.table_state.selected(), None); } + + // --- selected_executor tests --- + + #[test] + fn selected_executor_none_when_no_selection() { + let data = make_executors_data( + vec![make_executor("host", 8080, "id-a", 1)], + SortColumn::None, + SortOrder::Ascending, + ); + assert!(data.selected_executor().is_none()); + } + + #[test] + fn selected_executor_returns_correct_executor() { + let mut data = make_executors_data( + vec![ + make_executor("host", 8080, "id-a", 1), + make_executor("host", 8081, "id-b", 2), + ], + SortColumn::None, + SortOrder::Ascending, + ); + data.table_state.select(Some(1)); + assert_eq!(data.selected_executor().unwrap().id, "id-b"); + } + + // --- ExecutorDetailsPopup tests --- + + #[test] + fn executor_details_popup_new_scroll_position_is_zero() { + let popup = ExecutorDetailsPopup::new(make_executor_details("id-1")); + assert_eq!(popup.scroll_position, 0); + } + + #[test] + fn executor_details_popup_scroll_down_increments() { + let mut popup = ExecutorDetailsPopup::new(make_executor_details("id-1")); + popup.scroll_down(); + assert_eq!(popup.scroll_position, 1); + } + + #[test] + fn executor_details_popup_scroll_down_multiple_times() { + let mut popup = ExecutorDetailsPopup::new(make_executor_details("id-1")); + popup.scroll_down(); + popup.scroll_down(); + popup.scroll_down(); + assert_eq!(popup.scroll_position, 3); + } + + #[test] + fn executor_details_popup_scroll_up_decrements() { + let mut popup = ExecutorDetailsPopup::new(make_executor_details("id-1")); + popup.scroll_down(); + popup.scroll_down(); + popup.scroll_up(); + assert_eq!(popup.scroll_position, 1); + } + + #[test] + fn executor_details_popup_scroll_up_saturates_at_zero() { + let mut popup = ExecutorDetailsPopup::new(make_executor_details("id-1")); + popup.scroll_up(); + popup.scroll_up(); + assert_eq!(popup.scroll_position, 0); + } } diff --git a/ballista-cli/src/tui/event.rs b/ballista-cli/src/tui/event.rs index 999ad704cc..35c2d72fb4 100644 --- a/ballista-cli/src/tui/event.rs +++ b/ballista-cli/src/tui/event.rs @@ -19,6 +19,7 @@ use crossterm::event::{EventStream, KeyEvent}; use futures::{FutureExt, StreamExt}; use tokio::sync::mpsc; +use crate::tui::domain::executors::ExecutorDetails; use crate::tui::domain::{ SchedulerState, executors::Executor, @@ -37,6 +38,7 @@ pub enum UiData { JobDetails(JobDetails), JobStagesGraph(StagesGraph), JobStagesData(String, JobStagesResponse), + ExecutorDetails(ExecutorDetails), } #[derive(Clone, Debug)] diff --git a/ballista-cli/src/tui/http_client.rs b/ballista-cli/src/tui/http_client.rs index e35241be9a..f50cf4895e 100644 --- a/ballista-cli/src/tui/http_client.rs +++ b/ballista-cli/src/tui/http_client.rs @@ -20,6 +20,7 @@ use reqwest::{Client, Response}; use serde::de::DeserializeOwned; use std::time::Duration; +use crate::tui::domain::executors::ExecutorDetails; use crate::tui::{ TuiResult, domain::{ @@ -61,6 +62,12 @@ impl HttpClient { self.json::>(&url).await } + pub async fn get_executor(&self, executor_id: &str) -> TuiResult { + let url = self.url(format!("executor/{}", self.url_encode(executor_id)).as_str()); + tracing::trace!("Going to GET details for '{}'", &url); + self.json::(&url).await + } + pub async fn get_jobs(&self) -> TuiResult> { let url = self.url("jobs"); self.json::>(&url).await.map(|mut jobs| { diff --git a/ballista-cli/src/tui/mod.rs b/ballista-cli/src/tui/mod.rs index 1f17e50e61..f67740a4cf 100644 --- a/ballista-cli/src/tui/mod.rs +++ b/ballista-cli/src/tui/mod.rs @@ -31,7 +31,7 @@ use std::time::Duration; use terminal::TuiWrapper; use crate::tui::domain::{ - executors::ExecutorsData, + executors::{ExecutorDetailsPopup, ExecutorsData}, jobs::{JobsData, stages::JobStagesPopup}, metrics::MetricsData, }; @@ -114,6 +114,10 @@ pub async fn tui_main() -> TuiResult<()> { UiData::JobStagesData(job_id, stages) => { app.job_stages_popup = Some(JobStagesPopup::new(job_id, stages)); } + UiData::ExecutorDetails(executor) => { + app.executor_details_popup = + Some(ExecutorDetailsPopup::new(executor)); + } } } } diff --git a/ballista-cli/src/tui/ui/footer.rs b/ballista-cli/src/tui/ui/footer.rs index 367d8918ad..615f31cd1b 100644 --- a/ballista-cli/src/tui/ui/footer.rs +++ b/ballista-cli/src/tui/ui/footer.rs @@ -90,8 +90,22 @@ pub(super) fn render_footer(f: &mut Frame, area: Rect, app: &App) { .insert(0, Span::from("Current view key bindings: ")); } } else if app.is_executors_view() { - current_view_key_bindings - .push(Span::from("[1,2,...] Sort by first/second/... column, ")); + if app.is_executor_details_popup_open() { + current_view_key_bindings.push(Span::from("[↑↓] Scroll up/down, ")); + current_view_key_bindings.push(Span::from("[Esc] Close popup, ")); + } else { + if app.has_selected_executor() { + current_view_key_bindings + .push(Span::from("[Enter] View details, ")); + } + current_view_key_bindings.push(Span::from("[↑↓] Navigate, ")); + current_view_key_bindings + .push(Span::from("[1,2,...] Sort by first/second/... column, ")); + } + if !current_view_key_bindings.is_empty() { + current_view_key_bindings + .insert(0, Span::from("Current view key bindings: ")); + } } else if app.is_metrics_view() { current_view_key_bindings.push(Span::from("Metrics key bindings: ")); current_view_key_bindings.push(Span::from("[/] Search metrics, ")); diff --git a/ballista-cli/src/tui/ui/main/executors/executor_details_popup.rs b/ballista-cli/src/tui/ui/main/executors/executor_details_popup.rs new file mode 100644 index 0000000000..757f8ad691 --- /dev/null +++ b/ballista-cli/src/tui/ui/main/executors/executor_details_popup.rs @@ -0,0 +1,178 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::tui::app::App; +use crate::tui::domain::executors::{ExecutorDetails, ExecutorDetailsPopup}; +use ratatui::Frame; +use ratatui::prelude::{Color, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph}; + +pub(crate) fn render_executor_details_popup(f: &mut Frame, app: &App) { + let Some(popup) = &app.executor_details_popup else { + return; + }; + + let area = crate::tui::ui::centered_rect(35, 55, f.area()); + f.render_widget(Clear, area); + + let executor = &popup.executor; + let title = " Executor details "; + + let lines = build_lines(app, popup, executor); + + let block = Block::default() + .title(title) + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::LightCyan)) + .border_type(BorderType::Thick); + + let paragraph = Paragraph::new(lines) + .block(block) + .scroll((popup.scroll_position, 0)); + + f.render_widget(paragraph, area); +} + +fn build_lines<'a>( + app: &'a App, + _popup: &'a ExecutorDetailsPopup, + executor_details: &'a ExecutorDetails, +) -> Vec> { + let executor = &executor_details.executor_info; + let label_style = Style::default().fg(Color::Yellow); + + let mut lines = vec![ + Line::from(vec![ + Span::styled(" Address ", label_style), + Span::raw(format!("{}:{}", executor.host, executor.port)), + ]), + Line::from(vec![ + Span::styled(" ID ", label_style), + Span::raw(executor.id.clone()), + ]), + Line::from(vec![ + Span::styled(" Last Seen ", label_style), + Span::raw(app.format_datetime(executor.last_seen)), + ]), + Line::from(vec![ + Span::styled(" Task Slots ", label_style), + Span::raw(executor.specification.task_slots.to_string()), + ]), + ]; + + if !executor.metrics.is_empty() { + lines.push(Line::from("")); + lines.push(Line::from(vec![Span::styled(" Metrics", label_style)])); + for metric in &executor.metrics { + lines.push(Line::from(vec![ + Span::styled( + format!(" {:<20} ", metric_name(&metric.typ)), + label_style, + ), + Span::raw(app.format_size(metric.value as usize)), + ])); + } + } + + let os = &executor_details.os_info; + lines.push(Line::from("")); + lines.push(Line::from(vec![Span::styled(" OS Info", label_style)])); + lines.push(Line::from(vec![ + Span::styled(" System Name ", label_style), + Span::raw(os.system_name.clone()), + ])); + lines.push(Line::from(vec![ + Span::styled(" OS Version ", label_style), + Span::raw(os.os_ver.clone()), + ])); + lines.push(Line::from(vec![ + Span::styled(" OS Version Long ", label_style), + Span::raw(os.os_ver_long.clone()), + ])); + lines.push(Line::from(vec![ + Span::styled(" Kernel Version ", label_style), + Span::raw(os.kernel_ver.clone()), + ])); + lines.push(Line::from(vec![ + Span::styled(" Physical Cores ", label_style), + Span::raw(os.physical_cores.to_string()), + ])); + lines.push(Line::from(vec![ + Span::styled(" Num Disks ", label_style), + Span::raw(os.num_disks.to_string()), + ])); + lines.push(Line::from(vec![ + Span::styled(" Total Disk ", label_style), + Span::raw(app.format_size(os.total_disk_space as usize)), + ])); + lines.push(Line::from(vec![ + Span::styled(" Available Disk ", label_style), + Span::raw(app.format_size(os.total_available_disk_space as usize)), + ])); + lines.push(Line::from(vec![ + Span::styled(" Open Files Limit ", label_style), + Span::raw(os.open_files_limit.to_string()), + ])); + + lines +} + +fn metric_name(typ: &str) -> String { + match typ { + "proc_physical_memory" => "Physical Memory".to_string(), + "proc_virtual_memory" => "Virtual Memory".to_string(), + "peak_physical_memory" => "Peak Physical Memory".to_string(), + "peak_virtual_memory" => "Peak Virtual Memory".to_string(), + _ => typ.to_string(), + } +} + +#[cfg(test)] +mod tests { + use super::metric_name; + + #[test] + fn metric_name_proc_physical_memory() { + assert_eq!(metric_name("proc_physical_memory"), "Physical Memory"); + } + + #[test] + fn metric_name_proc_virtual_memory() { + assert_eq!(metric_name("proc_virtual_memory"), "Virtual Memory"); + } + + #[test] + fn metric_name_peak_physical_memory() { + assert_eq!(metric_name("peak_physical_memory"), "Peak Physical Memory"); + } + + #[test] + fn metric_name_peak_virtual_memory() { + assert_eq!(metric_name("peak_virtual_memory"), "Peak Virtual Memory"); + } + + #[test] + fn metric_name_unknown_type_returns_as_is() { + assert_eq!(metric_name("cpu_usage"), "cpu_usage"); + } + + #[test] + fn metric_name_empty_string_returns_empty() { + assert_eq!(metric_name(""), ""); + } +} diff --git a/ballista-cli/src/tui/ui/main/executors/mod.rs b/ballista-cli/src/tui/ui/main/executors/mod.rs index 9a372b0cc4..a5cf2fd3cb 100644 --- a/ballista-cli/src/tui/ui/main/executors/mod.rs +++ b/ballista-cli/src/tui/ui/main/executors/mod.rs @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +pub mod executor_details_popup; mod executors_table; mod jobs; @@ -32,6 +33,14 @@ use crate::tui::{ event::{Event, UiData}, }; +pub async fn load_executor_details_popup(app: &App, executor_id: &str) -> TuiResult<()> { + let executor = app.http_client.get_executor(executor_id).await?; + app.send_event(Event::DataLoaded { + data: UiData::ExecutorDetails(executor), + }) + .await +} + pub fn render_executors(f: &mut Frame, area: Rect, app: &App) { f.render_widget(Clear, area); diff --git a/ballista-cli/src/tui/ui/main/mod.rs b/ballista-cli/src/tui/ui/main/mod.rs index 6b584d9956..5740500149 100644 --- a/ballista-cli/src/tui/ui/main/mod.rs +++ b/ballista-cli/src/tui/ui/main/mod.rs @@ -19,7 +19,10 @@ mod executors; mod jobs; mod metrics; -pub use executors::{load_executors_data, render_executors}; +pub use executors::{ + executor_details_popup, load_executor_details_popup, load_executors_data, + render_executors, +}; pub use jobs::{ job_dot_popup, job_plan_popup, job_stages_popup, load_job_details, load_job_dot, load_job_stages_popup, load_jobs_data, render_jobs, stage_plan_popup, diff --git a/ballista-cli/src/tui/ui/mod.rs b/ballista-cli/src/tui/ui/mod.rs index 5562e4f8e0..dbe69a7d9b 100644 --- a/ballista-cli/src/tui/ui/mod.rs +++ b/ballista-cli/src/tui/ui/mod.rs @@ -28,9 +28,10 @@ use crate::tui::app::App; use crate::tui::ui::header::render_header; use footer::render_footer; pub use main::{ - job_dot_popup, job_plan_popup, job_stages_popup, load_executors_data, - load_job_details, load_job_dot, load_job_stages_popup, load_jobs_data, - load_metrics_data, stage_plan_popup, stage_tasks_popup, + executor_details_popup, job_dot_popup, job_plan_popup, job_stages_popup, + load_executor_details_popup, load_executors_data, load_job_details, load_job_dot, + load_job_stages_popup, load_jobs_data, load_metrics_data, stage_plan_popup, + stage_tasks_popup, }; use main::{render_executors, render_jobs, render_metrics}; use ratatui::{ @@ -71,6 +72,8 @@ pub(crate) fn render(f: &mut Frame, app: &App) { job_dot_popup::render_job_dot_popup(f, app); } else if app.job_plan_popup.is_some() { job_plan_popup::render_job_plan_popup(f, app); + } else if app.executor_details_popup.is_some() { + executor_details_popup::render_executor_details_popup(f, app); } }