This is an automated email from the ASF dual-hosted git repository.

martin-g pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-ballista.git


The following commit(s) were added to refs/heads/main by this push:
     new 083c57285 feat: TUI shows running job information (#1717)
083c57285 is described below

commit 083c57285120e38969c92996a4470354806ddace
Author: Marko Milenković <[email protected]>
AuthorDate: Mon May 18 03:51:32 2026 +0100

    feat: TUI shows running job information (#1717)
    
    * feat: TUI shows running job information
    
    * Add unit tests for App::is_selected_job_completed_or_running()
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    * Add serde(default) for JobStageResponse::tasks too
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    
    ---------
    
    Signed-off-by: Martin Tzvetanov Grigorov <[email protected]>
    Co-authored-by: Martin Tzvetanov Grigorov <[email protected]>
---
 ballista-cli/src/tui/app.rs                        | 77 ++++++++++++++++++----
 ballista-cli/src/tui/domain/jobs/stages.rs         | 15 +++--
 ballista-cli/src/tui/ui/footer.rs                  |  2 +-
 .../src/tui/ui/main/jobs/job_stages_popup.rs       | 40 ++++++-----
 .../src/tui/ui/main/jobs/stage_tasks_popup.rs      | 25 ++++---
 5 files changed, 117 insertions(+), 42 deletions(-)

diff --git a/ballista-cli/src/tui/app.rs b/ballista-cli/src/tui/app.rs
index 8701ea998..1000afe40 100644
--- a/ballista-cli/src/tui/app.rs
+++ b/ballista-cli/src/tui/app.rs
@@ -542,10 +542,10 @@ impl App {
         self.jobs_data.selected_job(&self.search_term).is_some()
     }
 
-    pub fn is_selected_job_completed(&self) -> bool {
+    pub fn is_selected_job_completed_or_running(&self) -> bool {
         self.jobs_data
             .selected_job(&self.search_term)
-            .is_some_and(|j| j.status == "Completed")
+            .is_some_and(|j| j.status == "Completed" || j.status == "Running")
     }
 
     pub fn is_selected_job_cancelable(&self) -> bool {
@@ -589,11 +589,9 @@ impl App {
     }
 
     fn open_job_plan_popup(&mut self) {
-        let is_completed = self
-            .jobs_data
-            .selected_job(&self.search_term)
-            .is_some_and(|j| j.status == "Completed");
-        if is_completed && let Some(details) = &self.job_details {
+        if self.is_selected_job_completed_or_running()
+            && let Some(details) = &self.job_details
+        {
             self.job_plan_popup =
                 Some(JobPlansPopup::new(details.clone(), PlanTab::Stage));
         }
@@ -789,20 +787,45 @@ mod tests {
         assert!(app.has_more_than_one_job());
     }
 
+    // --- open_job_plan_popup tests ---
+
+    fn make_job_details(job_id: &str) -> crate::tui::domain::jobs::JobDetails {
+        crate::tui::domain::jobs::JobDetails {
+            job_id: job_id.to_string(),
+            logical_plan: Some("logical".to_string()),
+            physical_plan: Some("physical".to_string()),
+            stage_plan: Some("stage".to_string()),
+        }
+    }
+
+    #[test]
+    fn open_job_plan_popup_opens_for_running_job_with_details() {
+        let mut app = make_app();
+        app.jobs_data.jobs = vec![make_job("j1", "Running")];
+        app.jobs_data.table_state.select(Some(0));
+        app.job_details = Some(make_job_details("j1"));
+        app.open_job_plan_popup();
+        assert!(app.job_plan_popup.is_some());
+    }
+
     #[test]
-    fn is_selected_job_completed_true_when_completed_job_selected() {
+    fn open_job_plan_popup_opens_for_completed_job_with_details() {
         let mut app = make_app();
         app.jobs_data.jobs = vec![make_job("j1", "Completed")];
         app.jobs_data.table_state.select(Some(0));
-        assert!(app.is_selected_job_completed());
+        app.job_details = Some(make_job_details("j1"));
+        app.open_job_plan_popup();
+        assert!(app.job_plan_popup.is_some());
     }
 
     #[test]
-    fn is_selected_job_completed_false_for_running_job() {
+    fn open_job_plan_popup_does_not_open_without_details() {
         let mut app = make_app();
         app.jobs_data.jobs = vec![make_job("j1", "Running")];
         app.jobs_data.table_state.select(Some(0));
-        assert!(!app.is_selected_job_completed());
+        app.job_details = None;
+        app.open_job_plan_popup();
+        assert!(app.job_plan_popup.is_none());
     }
 
     // --- sort_jobs_by toggle tests ---
@@ -1057,4 +1080,36 @@ mod tests {
         app.jobs_data.table_state.select(Some(0));
         assert!(!app.is_selected_job_cancelable());
     }
+
+    #[test]
+    fn is_selected_job_completed_or_running_for_completed() {
+        let mut app = make_app();
+        app.jobs_data.jobs = vec![make_job("j1", "Completed")];
+        app.jobs_data.table_state.select(Some(0));
+        assert!(app.is_selected_job_completed_or_running());
+    }
+
+    #[test]
+    fn is_selected_job_completed_or_running_for_running() {
+        let mut app = make_app();
+        app.jobs_data.jobs = vec![make_job("j1", "Running")];
+        app.jobs_data.table_state.select(Some(0));
+        assert!(app.is_selected_job_completed_or_running());
+    }
+
+    #[test]
+    fn is_selected_job_completed_or_running_for_queued() {
+        let mut app = make_app();
+        app.jobs_data.jobs = vec![make_job("j1", "Queued")];
+        app.jobs_data.table_state.select(Some(0));
+        assert!(!app.is_selected_job_completed_or_running());
+    }
+
+    #[test]
+    fn is_selected_job_completed_or_running_for_failed() {
+        let mut app = make_app();
+        app.jobs_data.jobs = vec![make_job("j1", "Failed")];
+        app.jobs_data.table_state.select(Some(0));
+        assert!(!app.is_selected_job_completed_or_running());
+    }
 }
diff --git a/ballista-cli/src/tui/domain/jobs/stages.rs 
b/ballista-cli/src/tui/domain/jobs/stages.rs
index b6aea823a..824217471 100644
--- a/ballista-cli/src/tui/domain/jobs/stages.rs
+++ b/ballista-cli/src/tui/domain/jobs/stages.rs
@@ -29,14 +29,17 @@ pub struct JobStageResponse {
     pub id: String,
     #[serde(rename = "stage_status")]
     pub status: String,
-    #[serde(rename = "stage_plan")]
+    #[serde(rename = "stage_plan", default)]
     pub plan: String,
     pub input_rows: usize,
     pub output_rows: usize,
     pub elapsed_compute: Option<String>,
-    pub task_duration_percentiles: TaskPercentiles,
-    pub task_input_percentiles: TaskPercentiles,
-    pub tasks: Vec<StageTaskResponse>,
+    #[serde(default)]
+    pub task_duration_percentiles: Option<TaskPercentiles>,
+    #[serde(default)]
+    pub task_input_percentiles: Option<TaskPercentiles>,
+    #[serde(default)]
+    pub tasks: Vec<Option<StageTaskResponse>>,
 }
 
 // TaskSummary
@@ -246,8 +249,8 @@ mod tests {
             input_rows: 0,
             output_rows: 0,
             elapsed_compute: Some("1ns".to_string()),
-            task_duration_percentiles: make_percentiles(),
-            task_input_percentiles: make_percentiles(),
+            task_duration_percentiles: Some(make_percentiles()),
+            task_input_percentiles: Some(make_percentiles()),
             tasks: Vec::new(),
         }
     }
diff --git a/ballista-cli/src/tui/ui/footer.rs 
b/ballista-cli/src/tui/ui/footer.rs
index ffcad51ef..27a65073f 100644
--- a/ballista-cli/src/tui/ui/footer.rs
+++ b/ballista-cli/src/tui/ui/footer.rs
@@ -66,7 +66,7 @@ pub(super) fn render_footer(f: &mut Frame, area: Rect, app: 
&App) {
                                     .push(Span::from("[c] Cancel job, "));
                             }
 
-                            if app.is_selected_job_completed() {
+                            if app.is_selected_job_completed_or_running() {
                                 current_view_key_bindings
                                     .push(Span::from("[p] View job plans, "));
                             }
diff --git a/ballista-cli/src/tui/ui/main/jobs/job_stages_popup.rs 
b/ballista-cli/src/tui/ui/main/jobs/job_stages_popup.rs
index 874b24e9f..f0b641a13 100644
--- a/ballista-cli/src/tui/ui/main/jobs/job_stages_popup.rs
+++ b/ballista-cli/src/tui/ui/main/jobs/job_stages_popup.rs
@@ -97,23 +97,31 @@ fn build_stage_row(i: usize, stage: &JobStageResponse, app: 
&App) -> Row<'static
         _ => Color::Gray,
     };
 
-    let p = &stage.task_duration_percentiles;
-    let duration_percentiles = format!(
-        "{}/{}/{}/{}/{}",
-        app.format_duration(p.min),
-        app.format_duration(p.p25),
-        app.format_duration(p.median),
-        app.format_duration(p.p75),
-        app.format_duration(p.max)
+    let duration_percentiles = 
stage.task_duration_percentiles.as_ref().map_or_else(
+        || "N/A".to_string(),
+        |p| {
+            format!(
+                "{}/{}/{}/{}/{}",
+                app.format_duration(p.min),
+                app.format_duration(p.p25),
+                app.format_duration(p.median),
+                app.format_duration(p.p75),
+                app.format_duration(p.max)
+            )
+        },
     );
-    let p = &stage.task_input_percentiles;
-    let input_percentiles = format!(
-        "{}/{}/{}/{}/{}",
-        app.format_count(p.min.try_into().unwrap_or(0)),
-        app.format_count(p.p25.try_into().unwrap_or(0)),
-        app.format_count(p.median.try_into().unwrap_or(0)),
-        app.format_count(p.p75.try_into().unwrap_or(0)),
-        app.format_count(p.max.try_into().unwrap_or(0))
+    let input_percentiles = stage.task_input_percentiles.as_ref().map_or_else(
+        || "N/A".to_string(),
+        |p| {
+            format!(
+                "{}/{}/{}/{}/{}",
+                app.format_count(p.min.try_into().unwrap_or(0)),
+                app.format_count(p.p25.try_into().unwrap_or(0)),
+                app.format_count(p.median.try_into().unwrap_or(0)),
+                app.format_count(p.p75.try_into().unwrap_or(0)),
+                app.format_count(p.max.try_into().unwrap_or(0))
+            )
+        },
     );
 
     Row::new(vec![
diff --git a/ballista-cli/src/tui/ui/main/jobs/stage_tasks_popup.rs 
b/ballista-cli/src/tui/ui/main/jobs/stage_tasks_popup.rs
index c04bf45a7..7b371da91 100644
--- a/ballista-cli/src/tui/ui/main/jobs/stage_tasks_popup.rs
+++ b/ballista-cli/src/tui/ui/main/jobs/stage_tasks_popup.rs
@@ -60,6 +60,7 @@ pub(crate) fn render_stage_tasks_popup(f: &mut Frame, app: 
&App) {
     let rows = stage
         .tasks
         .iter()
+        .flatten()
         .enumerate()
         .map(|(i, task)| build_stage_task_row(i, task, app));
 
@@ -121,20 +122,28 @@ fn build_stage_task_row(i: usize, task: 
&StageTaskResponse, app: &App) -> Row<'s
         Cell::from(Text::from(task.partition_id.to_string()).right_aligned()),
         Cell::from(Text::from(format_datetime(task.scheduled_time, 
app)).centered()),
         Cell::from(
-            Text::from(app.format_duration(task.launch_time - 
task.scheduled_time))
-                .right_aligned(),
+            Text::from(
+                
app.format_duration(task.launch_time.saturating_sub(task.scheduled_time)),
+            )
+            .right_aligned(),
         ),
         Cell::from(
-            Text::from(app.format_duration(task.start_exec_time - 
task.scheduled_time))
-                .right_aligned(),
+            Text::from(app.format_duration(
+                task.start_exec_time.saturating_sub(task.scheduled_time),
+            ))
+            .right_aligned(),
         ),
         Cell::from(
-            Text::from(app.format_duration(task.end_exec_time - 
task.start_exec_time))
-                .right_aligned(),
+            Text::from(app.format_duration(
+                task.end_exec_time.saturating_sub(task.start_exec_time),
+            ))
+            .right_aligned(),
         ),
         Cell::from(
-            Text::from(app.format_duration(task.finish_time - 
task.scheduled_time))
-                .right_aligned(),
+            Text::from(
+                
app.format_duration(task.finish_time.saturating_sub(task.scheduled_time)),
+            )
+            .right_aligned(),
         ),
     ])
     .style(Style::default().bg(bg))


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to