You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by mg...@apache.org on 2026/05/15 10:00:15 UTC
(datafusion-ballista) branch main updated: [TUI] Add a config setting for rendering job stage's plan as a tree (#1704)
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 c05f1dc31 [TUI] Add a config setting for rendering job stage's plan as a tree (#1704)
c05f1dc31 is described below
commit c05f1dc31b17f6ffa6d4bd3bb5f82467a39c7074
Author: Martin Grigorov <ma...@users.noreply.github.com>
AuthorDate: Fri May 15 13:00:11 2026 +0300
[TUI] Add a config setting for rendering job stage's plan as a tree (#1704)
* [TUI] Add a config setting for rendering job stage's plan as a tree
Related-to: #1650
* Stage tasks popup is not scrollable
* Fix the docs for Stage Plan popup scrolling
* Reset the scroll position when showing a stage plan
* Remove debug leftover
* Encapsulate the plan popup scroll position
* url encode the job_id for cancel_job http request
---
ballista-cli/src/tui/app.rs | 11 +++--
ballista-cli/src/tui/domain/jobs/stages.rs | 57 ++++++++++++++--------
ballista-cli/src/tui/http_client.rs | 32 ++++++------
ballista-cli/src/tui/infrastructure/config.rs | 25 ++++++++++
ballista-cli/src/tui/mod.rs | 2 +-
ballista-cli/src/tui/ui/footer.rs | 9 ++--
ballista-cli/src/tui/ui/main/jobs/mod.rs | 11 ++---
.../src/tui/ui/main/jobs/stage_plan_popup.rs | 4 +-
docs/source/user-guide/cli.md | 15 +++++-
9 files changed, 112 insertions(+), 54 deletions(-)
diff --git a/ballista-cli/src/tui/app.rs b/ballista-cli/src/tui/app.rs
index 663087cc5..7d37baf24 100644
--- a/ballista-cli/src/tui/app.rs
+++ b/ballista-cli/src/tui/app.rs
@@ -201,10 +201,13 @@ impl App {
&& let KeyCode::Esc = key.code
{
popup.set_no_details_view();
- } else if popup.is_plan_view()
- && let KeyCode::Esc = key.code
- {
- popup.set_no_details_view();
+ } else if popup.is_plan_view() {
+ match key.code {
+ KeyCode::Esc => popup.set_no_details_view(),
+ KeyCode::Up => popup.scroll_up(),
+ KeyCode::Down => popup.scroll_down(),
+ _ => {}
+ }
} else if popup.is_no_details_view() {
match key.code {
KeyCode::Up => popup.scroll_up(),
diff --git a/ballista-cli/src/tui/domain/jobs/stages.rs b/ballista-cli/src/tui/domain/jobs/stages.rs
index f4d6aaad2..33d4a28b6 100644
--- a/ballista-cli/src/tui/domain/jobs/stages.rs
+++ b/ballista-cli/src/tui/domain/jobs/stages.rs
@@ -77,6 +77,7 @@ pub struct JobStagesPopup {
pub stages: JobStagesResponse,
pub table_state: TableState,
details_view: StageDetailsView,
+ scroll_position: u16,
}
impl JobStagesPopup {
@@ -86,15 +87,21 @@ impl JobStagesPopup {
stages,
table_state: TableState::default(),
details_view: StageDetailsView::None,
+ scroll_position: 0,
}
}
+ pub fn plan_scroll_position(&self) -> u16 {
+ self.scroll_position
+ }
+
pub fn set_tasks_view(&mut self) {
self.details_view = StageDetailsView::Tasks;
}
pub fn set_plan_view(&mut self) {
self.details_view = StageDetailsView::Plan;
+ self.scroll_position = 0;
}
pub fn set_no_details_view(&mut self) {
@@ -114,36 +121,44 @@ impl JobStagesPopup {
}
pub fn scroll_down(&mut self) {
- let len = self.stages.stages.len();
- if len == 0 {
- self.table_state.select(None);
- return;
- }
- if let Some(selected) = self.table_state.selected() {
- if selected < len - 1 {
- self.table_state.select(Some(selected + 1));
- } else {
+ if self.is_no_details_view() {
+ let len = self.stages.stages.len();
+ if len == 0 {
self.table_state.select(None);
+ return;
}
- } else {
- self.table_state.select(Some(0));
+ if let Some(selected) = self.table_state.selected() {
+ if selected < len - 1 {
+ self.table_state.select(Some(selected + 1));
+ } else {
+ self.table_state.select(None);
+ }
+ } else {
+ self.table_state.select(Some(0));
+ }
+ } else if self.is_plan_view() {
+ self.scroll_position = self.scroll_position.saturating_add(1);
}
}
pub fn scroll_up(&mut self) {
- let len = self.stages.stages.len();
- if len == 0 {
- self.table_state.select(None);
- return;
- }
- if let Some(selected) = self.table_state.selected() {
- if selected == 0 {
+ if self.is_no_details_view() {
+ let len = self.stages.stages.len();
+ if len == 0 {
self.table_state.select(None);
+ return;
+ }
+ if let Some(selected) = self.table_state.selected() {
+ if selected == 0 {
+ self.table_state.select(None);
+ } else {
+ self.table_state.select(Some(selected - 1));
+ }
} else {
- self.table_state.select(Some(selected - 1));
+ self.table_state.select(Some(len - 1));
}
- } else {
- self.table_state.select(Some(len - 1));
+ } else if self.is_plan_view() {
+ self.scroll_position = self.scroll_position.saturating_sub(1);
}
}
diff --git a/ballista-cli/src/tui/http_client.rs b/ballista-cli/src/tui/http_client.rs
index 5f142cd9a..1aa1862e9 100644
--- a/ballista-cli/src/tui/http_client.rs
+++ b/ballista-cli/src/tui/http_client.rs
@@ -34,22 +34,22 @@ use crate::tui::{
};
pub struct HttpClient {
- scheduler_url: String,
+ config: Settings,
client: reqwest::Client,
}
impl HttpClient {
pub fn new(config: Settings) -> TuiResult<Self> {
Ok(Self {
- scheduler_url: config.scheduler.url,
client: Client::builder()
.timeout(Duration::from_millis(config.http.timeout))
.build()?,
+ config,
})
}
pub fn scheduler_url(&self) -> &str {
- &self.scheduler_url
+ &self.config.scheduler.url
}
pub async fn get_scheduler_state(&self) -> TuiResult<SchedulerState> {
@@ -77,7 +77,7 @@ impl HttpClient {
}
pub async fn cancel_job(&self, job_id: &str) -> TuiResult<CancelJobResponse> {
- let url = format!("{}/api/job/{}", self.scheduler_url, job_id);
+ let url = self.url(&format!("job/{}", self.url_encode(job_id)));
tracing::trace!("Going to PATCH {}", &url);
let response = self
.client
@@ -110,7 +110,7 @@ impl HttpClient {
stage_plan: Option<String>,
}
- let url = format!("{}/api/job/{}", self.scheduler_url, self.url_encode(job_id));
+ let url = self.url(&format!("job/{}", self.url_encode(job_id)));
let resp = self.json::<JobDetailResponse>(&url).await?;
Ok(JobDetails {
job_id: job_id.to_string(),
@@ -121,20 +121,20 @@ impl HttpClient {
}
pub async fn get_job_dot(&self, job_id: &str) -> TuiResult<String> {
- let url = format!(
- "{}/api/job/{}/dot",
- self.scheduler_url,
- self.url_encode(job_id)
- );
+ let url = self.url(&format!("job/{}/dot", self.url_encode(job_id)));
self.text(&url).await
}
pub async fn get_job_stages(&self, job_id: &str) -> TuiResult<JobStagesResponse> {
- let url = format!(
- "{}/api/job/{}/stages",
- self.scheduler_url,
- self.url_encode(job_id)
- );
+ let url = self.url(&format!(
+ "job/{}/stages{}",
+ self.url_encode(job_id),
+ if self.config.job.stage.plan.tree {
+ "?render_tree=true"
+ } else {
+ ""
+ }
+ ));
self.json::<JobStagesResponse>(&url).await
}
@@ -190,7 +190,7 @@ impl HttpClient {
}
fn url(&self, path: &str) -> String {
- format!("{}/api/{}", self.scheduler_url, path)
+ format!("{}/api/{}", self.config.scheduler.url, path)
}
fn url_encode(&self, job_id: &str) -> String {
diff --git a/ballista-cli/src/tui/infrastructure/config.rs b/ballista-cli/src/tui/infrastructure/config.rs
index 323e0e5a3..7cf001c85 100644
--- a/ballista-cli/src/tui/infrastructure/config.rs
+++ b/ballista-cli/src/tui/infrastructure/config.rs
@@ -29,12 +29,32 @@ pub struct HttpSettings {
pub timeout: u64,
}
+#[derive(Debug, Deserialize)]
+pub struct JobSettings {
+ /// The job stages' settings
+ pub stage: JobStageSettings,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct JobStageSettings {
+ /// The job plan's settings
+ pub plan: JobPlanSettings,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct JobPlanSettings {
+ /// Whether to render the plan as a tree.
+ pub tree: bool,
+}
+
#[derive(Debug, Deserialize)]
pub struct Settings {
pub scheduler: SchedulerSettings,
pub http: HttpSettings,
/// How often to refresh the UI. In millis.
pub tick_interval_ms: u64,
+
+ pub job: JobSettings,
}
const DEFAULT_CONFIG: &str = r#"
@@ -45,6 +65,11 @@ scheduler:
http:
timeout: 2000
+
+job:
+ stage:
+ plan:
+ tree: false
"#;
impl Settings {
diff --git a/ballista-cli/src/tui/mod.rs b/ballista-cli/src/tui/mod.rs
index 415afbc04..eb05ef001 100644
--- a/ballista-cli/src/tui/mod.rs
+++ b/ballista-cli/src/tui/mod.rs
@@ -54,7 +54,7 @@ pub async fn tui_main(tui_mode: Arc<AtomicBool>) -> TuiResult<()> {
let (app_tx, mut app_rx) = tokio::sync::mpsc::channel(16);
app.set_event_tx(app_tx);
- let _ = crate::tui::ui::load_executors_data(&app).await;
+ let _ = ui::load_executors_data(&app).await;
loop {
tui_wrapper.terminal.draw(|f| ui::render(f, &app))?;
diff --git a/ballista-cli/src/tui/ui/footer.rs b/ballista-cli/src/tui/ui/footer.rs
index 615f31cd1..ffcad51ef 100644
--- a/ballista-cli/src/tui/ui/footer.rs
+++ b/ballista-cli/src/tui/ui/footer.rs
@@ -77,9 +77,12 @@ pub(super) fn render_footer(f: &mut Frame, area: Rect, app: &App) {
current_view_key_bindings.push(Span::from("[p] View plan, "));
current_view_key_bindings
.push(Span::from("[Esc] Close popup, "));
- } else if app.is_job_stage_tasks_popup_open()
- || app.is_job_stage_plan_popup_open()
- {
+ } else if app.is_job_stage_plan_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.is_job_stage_tasks_popup_open() {
current_view_key_bindings
.push(Span::from("[Esc] Close popup, "));
}
diff --git a/ballista-cli/src/tui/ui/main/jobs/mod.rs b/ballista-cli/src/tui/ui/main/jobs/mod.rs
index a6b3955a1..12b951dd3 100644
--- a/ballista-cli/src/tui/ui/main/jobs/mod.rs
+++ b/ballista-cli/src/tui/ui/main/jobs/mod.rs
@@ -46,13 +46,10 @@ use ratatui::{
};
pub async fn load_jobs_data(app: &App) -> TuiResult<()> {
- let jobs = match app.http_client.get_jobs().await {
- Ok(jobs) => jobs,
- Err(e) => {
- tracing::error!("Failed to load the jobs: {e:?}");
- Vec::new()
- }
- };
+ let jobs = app.http_client.get_jobs().await.unwrap_or_else(|e| {
+ tracing::error!("Failed to load the jobs: {e:?}");
+ Vec::new()
+ });
app.send_event(Event::DataLoaded {
data: UiData::Jobs(jobs),
diff --git a/ballista-cli/src/tui/ui/main/jobs/stage_plan_popup.rs b/ballista-cli/src/tui/ui/main/jobs/stage_plan_popup.rs
index c95a32c71..57bd99034 100644
--- a/ballista-cli/src/tui/ui/main/jobs/stage_plan_popup.rs
+++ b/ballista-cli/src/tui/ui/main/jobs/stage_plan_popup.rs
@@ -50,7 +50,9 @@ fn render_plans(f: &mut Frame, area: Rect, app: &App) {
.border_style(Style::default().fg(Color::LightCyan))
.border_type(BorderType::Thick);
- let paragraph = Paragraph::new(stage.plan.clone()).block(block);
+ let paragraph = Paragraph::new(&*stage.plan)
+ .block(block)
+ .scroll((popup.plan_scroll_position(), 0));
f.render_widget(paragraph, area);
}
diff --git a/docs/source/user-guide/cli.md b/docs/source/user-guide/cli.md
index 113601355..3da44fccc 100644
--- a/docs/source/user-guide/cli.md
+++ b/docs/source/user-guide/cli.md
@@ -206,7 +206,14 @@ The TUI provides the following views:
| `p` | View execution plan for the selected stage |
| `Esc` | Close popup |
-#### Stage Tasks / Plan Popup Keybindings
+#### Stage Plan Popup Keybindings
+
+| Key | Action |
+| --------- | -------------------------- |
+| `↑` / `↓` | Scroll up/down |
+| `Esc` | Return to Job Stages popup |
+
+#### Stage Tasks Popup Keybindings
| Key | Action |
| ----- | -------------------------- |
@@ -264,6 +271,11 @@ Create the file manually if it does not exist. The following settings are availa
```yaml
tick_interval_ms: 2000
+job:
+ stage:
+ plan:
+ tree: false
+
scheduler:
url: http://localhost:50050
@@ -272,6 +284,7 @@ http:
```
- `tick_interval_ms`: How often the TUI refreshes data from the scheduler (milliseconds).
+- `job.stage.plan.tree`: Whether to show the stage execution plan as a tree.
- `scheduler.url`: The Ballista scheduler HTTP endpoint.
- `http.timeout`: HTTP request timeout in milliseconds.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org