Skip to content

1828: feat(TUI): enable various plan rendering formats - #68

Open
martin-augment wants to merge 7 commits into
mainfrom
pr-1828-2026-06-09-07-33-34
Open

1828: feat(TUI): enable various plan rendering formats#68
martin-augment wants to merge 7 commits into
mainfrom
pr-1828-2026-06-09-07-33-34

Conversation

@martin-augment

Copy link
Copy Markdown
Owner

1828: To review by AI

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR implements multi-format plan selection for the Ballista TUI job details interface. It introduces PhysicalFormat (default/tree) and StagePlanTab (default/tree/metrics) enums with caching infrastructure, updates HTTP client methods to accept optional plan_format query parameters, removes job configuration from settings, and adds keyboard-driven format/tab switching with corresponding UI updates. The feature spans both non-web (crossterm) and web (WASM) platforms, with dedicated keyboard handlers and async action emission for deferred HTTP loading.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr-1828-2026-06-09-07-33-34

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the Ballista CLI TUI by adding support for dynamic switching and rendering of different physical plan formats (such as tree format) and stage plans (default, tree, and metrics). It removes the static configuration setting in favor of interactive key bindings. The review feedback suggests two key improvements: mapping the default stage plan tab to None instead of Some("") to avoid sending empty query parameters (?plan_format=) to the server, and caching the fetched tree physical plan in self.job_details to prevent redundant network requests when the plan popup is closed and reopened.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +115 to +119
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Mapping StagePlanTab::Default to Some("") causes the HTTP client to append an empty query parameter ?plan_format= to the request URL. Mapping it to None instead will cleanly omit the query parameter, matching the default behavior of the endpoint.

Suggested change
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};
let fmt = match tab {
StagePlanTab::Default => None,
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Gemini AI reviewer is correct! Instead of passing an Option<&str> it would be better to pass &StagePlanTab and add implementations to it to represent itself as a query parameter and as String, i.e. a Display impl. This way the APIs will be typed and adding a new enum variant will be done in one place - StagePlanTab and its impls, not all over the codebase.

Comment on lines +360 to +364
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Mapping StagePlanTab::Default to Some("") causes the HTTP client to append an empty query parameter ?plan_format= to the request URL. Mapping it to None instead will cleanly omit the query parameter, matching the default behavior of the endpoint.

                let fmt = match tab {
                    StagePlanTab::Default => None,
                    StagePlanTab::Tree => Some("tree"),
                    StagePlanTab::Metrics => Some("metrics"),
                };

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Gemini AI reviewer is correct! Instead of passing an Option<&str> it would be better to pass &StagePlanTab and add implementations to it to represent itself as a query parameter and as String, i.e. a Display impl. This way the APIs will be typed and adding a new enum variant will be done in one place - StagePlanTab and its impls, not all over the codebase.

Comment on lines +858 to +864
if details.physical_plan_tree.is_some() {
if let Some(popup) = &mut self.job_plan_popup {
popup.details.physical_plan_tree = details.physical_plan_tree;
}
} else {
self.job_details = Some(details);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the tree physical plan is loaded, it is only updated in self.job_plan_popup. If the user closes and reopens the plan popup, the tree plan will have to be fetched again. Caching it in self.job_details as well avoids redundant network requests.

Suggested change
if details.physical_plan_tree.is_some() {
if let Some(popup) = &mut self.job_plan_popup {
popup.details.physical_plan_tree = details.physical_plan_tree;
}
} else {
self.job_details = Some(details);
}
if details.physical_plan_tree.is_some() {
if let Some(popup) = &mut self.job_plan_popup {
popup.details.physical_plan_tree = details.physical_plan_tree.clone();
}
if let Some(job_details) = &mut self.job_details {
if job_details.job_id == details.job_id {
job_details.physical_plan_tree = details.physical_plan_tree;
}
}
} else {
self.job_details = Some(details);
}

Comment on lines +344 to +348
Ok(details) => {
if let Some(p) = &mut self.job_plan_popup {
p.details.physical_plan_tree = details.physical_plan;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When the tree physical plan is fetched, it is only updated in self.job_plan_popup. Caching it in self.job_details as well ensures that if the user closes and reopens the plan popup, the tree plan does not need to be fetched again.

                    Ok(details) => {
                        if let Some(p) = &mut self.job_plan_popup {
                            p.details.physical_plan_tree = details.physical_plan.clone();
                        }
                        if let Some(job_details) = &mut self.job_details {
                            if job_details.job_id == job_id {
                                job_details.physical_plan_tree = details.physical_plan;
                            }
                        }
                    }

@claude

claude Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review

Overview

This PR adds dynamic plan rendering format support to the TUI: users can now switch between Default, Tree, and Metrics formats for stage plans (via d/t/m keys), and between Default and Tree for job-level physical plans (via d/t). It also removes the old static job.stage.plan.tree config boolean in favour of interactive controls, and refactors get_job_details/get_job_stages to accept a plan_format parameter.


Bugs / Correctness

1. StagePlanTab::Default maps to Some("") instead of None

In both ballista-cli/src/tui/ui/main/jobs/mod.rs (load_stage_plan) and ballista-cli/src/tui/mod.rs (LoadStagePlan handler):

StagePlanTab::Default => Some(""),

This produces ?plan_format= (empty value) in the request URL instead of omitting the parameter entirely. The server likely won't treat an empty plan_format the same as no plan_format. This should be None to match the original behaviour for the default format.

2. physical_plan_tree is always None from get_job_details

http_client.rs hardcodes physical_plan_tree: None when building a JobDetails from an HTTP response, then mod.rs works around this by doing:

details.physical_plan_tree = details.physical_plan.take();

The field rename happens outside the struct's natural construction site. If get_job_details is called anywhere else for tree format (or if the call site changes), the physical_plan_tree will silently remain None. The cleaner fix is to either populate physical_plan_tree inside get_job_details based on the format argument, or accept a callback/flag to decide which field to populate.


Code Quality

3. Broad lint suppressions

stages.rs adds a module-level #![allow(unfulfilled_lint_expectations)], which silences an entire lint class across the whole file. The same file also uses #[allow(dead_code)] on individual items. Prefer the narrowest scope possible; if a lint expectation is unfulfilled, that usually means the #[expect(...)] attribute should simply be removed rather than blanket-suppressed.

4. Dead-code markers on items that are actually used

StagePlanTab is annotated with #[allow(dead_code)] yet it is referenced extensively. Similarly, UiData::JobStagesPlanData and JobStagesPopup::set_tab carry #[allow(dead_code)]. These attributes suggest the code is partially wired up; it would be cleaner to complete the integration (or remove the unconnected paths) rather than silence the compiler.

5. Duplicated key-handling block in app.rs

The is_plan_view() key-dispatch block (handling d/t/m/Esc/arrows) appears twice — once in the non-web path and once in the web path — and is almost identical. Consider extracting a helper or a shared struct method so that a future change does not need to be applied in two places.

6. Option<()> as a needs-fetch signal

pub fn set_physical_format(&mut self, fmt: PhysicalFormat) -> Option<()> { ... }

Option<()> is legal but unusual. A plain bool named needs_fetch is easier to read at call sites:

// before
.map(|_| plans_popup.details.job_id.clone())
// after (with bool)
if plans_popup.set_physical_format(PhysicalFormat::Tree) { Some(plans_popup.details.job_id.clone()) } else { None }

Either is fine, but the current return type warrants the doc comment it already has — consider whether bool would be more self-documenting.


Performance / Design

7. PlanCache stores three full JobStagesResponse copies

Each format caches a complete JobStagesResponse, so stage data is potentially held in memory three times. For a TUI this is likely acceptable, but worth noting if stage responses can be large.

8. Inline use inside match arms

} else if popup.is_plan_view() {
    use crate::tui::domain::jobs::stages::StagePlanTab;

This use statement appears inside a method body to avoid a borrow-checker complaint, but the same import is already present at the top of the file in other compilation units. Moving the import to the top of the function (or the module) improves readability without changing semantics.


Test Coverage

No new unit tests are added for:

  • JobStagesPopup::set_tab (cache hit vs. cache miss paths)
  • JobStagesPopup::cache_plan_response (active format update + cache update)
  • JobPlansPopup::set_physical_format (needs-fetch logic)

The existing test helper in app.rs is correctly updated to include physical_plan_tree, which is good.


Minor / Nits

  • The footer hint for stage plan shows [d] Default format / [t] Tree format / [m] Show metrics — the inconsistent phrasing ("format" vs. "Show") is small but noticeable; consider aligning to [m] Metrics format.
  • get_job_stages and get_job_details now accept Option<&str> for format, but the values "tree" and "metrics" are repeated as string literals in multiple call sites. A small internal helper or a From<StagePlanTab> impl would make these more maintainable.

Positives

  • Removing the static JobSettings/JobStageSettings/JobPlanSettings config structs in favour of runtime controls is a clear simplification.
  • Embedding the active tab inside StageDetailsView::Plan(StagePlanTab) is an elegant way to keep the view state self-contained.
  • The caching approach (check cache first, fetch only on miss, populate on response) is well-structured and avoids redundant network calls.
  • Footer key-binding improvements are context-aware and more useful to the user.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5c57311. Configure here.

None
} else {
Some(tab)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale stage plan after tab

Medium Severity

When switching the stage plan tab to tree or metrics without a cached response, set_tab updates the active format but leaves stages unchanged until the HTTP call finishes. The plan popup keeps rendering the previous format’s stage_plan text, and a failed fetch leaves that mismatch with no loading or error state.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5c57311. Configure here.

@augmentcode

augmentcode Bot commented Jun 9, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: This PR extends the Ballista CLI TUI to support multiple plan rendering formats and to fetch/cycle them interactively.

Changes:

  • Adds PhysicalFormat and physical_plan_tree support so the job plan popup can toggle default vs tree physical plan rendering.
  • Adds stage plan tabs (Default/Tree/Metrics) and a simple cache to avoid refetching plans unnecessarily.
  • Extends HttpClient::get_job_details and HttpClient::get_job_stages with an optional plan_format query parameter.
  • Updates web and non-web event flows (WebKeyAsyncAction, UiData) to load and deliver stage-plan/tab-specific responses.
  • Updates footer key-binding hints for improved scrolling and the new stage plan interactions.

Technical Notes: Tree/metrics variants are loaded on-demand (only when selected) and then cached in the popup state to reduce repeated HTTP calls.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

WebKeyAsyncAction::LoadStagePlan(job_id, tab) => {
use crate::tui::domain::jobs::stages::StagePlanTab;
let fmt = match tab {
StagePlanTab::Default => Some(""),

@augmentcode augmentcode Bot Jun 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ballista-cli/src/tui/mod.rs:361: StagePlanTab::Default => Some("") will generate URLs like ...?plan_format= rather than omitting the parameter, which can behave differently on the server (e.g., treated as invalid/unknown format). Consider mapping the default case to None so the default request doesn’t send an empty query value.

Other locations where this applies: ballista-cli/src/tui/ui/main/jobs/mod.rs:116

Severity: medium

Other Locations
  • ballista-cli/src/tui/ui/main/jobs/mod.rs:116

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The Augment AI reviewer is correct! Instead of passing an Option<&str> it would be better to pass &StagePlanTab and add implementations to it to represent itself as a query parameter and as String, i.e. a Display impl. This way the APIs will be typed and adding a new enum variant will be done in one place - StagePlanTab and its impls, not all over the codebase.

current_view_key_bindings.push(Span::from("[↑↓] Scroll up/down, "));
current_view_key_bindings.push(Span::from("[↑↓←→] Scroll, "));
current_view_key_bindings.push(Span::from("[s] Stage plan, "));
current_view_key_bindings.push(Span::from("[p] Physical plan, "));

@augmentcode augmentcode Bot Jun 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ballista-cli/src/tui/ui/footer.rs:46: The job plan popup footer lists [s]/[p]/[l] but doesn’t mention the new physical plan format toggles (t/d), so users may miss that the format can be switched from the popup.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
ballista-cli/src/tui/app.rs (1)

857-864: ⚖️ Poor tradeoff

Consider explicit event variants for job details routing.

The current logic routes UiData::JobDetails based on whether physical_plan_tree is present, using field presence as an implicit signal. While functional, this coupling makes the routing logic less explicit. Consider using separate UiData variants (e.g., JobDetailsTree vs JobDetailsDefault) to make the intent clearer and reduce future maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/app.rs` around lines 857 - 864, The code currently
multiplexes UiData::JobDetails based on the presence of
details.physical_plan_tree, which couples routing to field content; change to
explicit variants (e.g., UiData::JobDetailsTree and UiData::JobDetailsDefault)
and update the match arm currently handling UiData::JobDetails to handle the new
variants: have UiData::JobDetailsTree carry the details intended for
job_plan_popup and assign popup.details.physical_plan_tree =
details.physical_plan_tree in that arm (using self.job_plan_popup), and have
UiData::JobDetailsDefault set self.job_details = Some(details); also update any
producers/emitters that create UiData::JobDetails to emit the correct new
variant so consumers like the match in app.rs no longer inspect
physical_plan_tree for routing.
ballista-cli/src/tui/http_client.rs (1)

118-125: 💤 Low value

Consider using proper URL query parameter construction.

The current implementation concatenates query parameters using string formatting. While functional, this approach is less robust than using a URL builder library method (e.g., Url::parse with query_pairs_mut). This would also handle URL encoding of parameter values automatically if needed in the future.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/http_client.rs` around lines 118 - 125, The code builds
the job URL by string-concatenating the query string; instead use a URL builder
so query parameters are added safely. Replace the manual formatting in the call
to self.url(...) for job/{...} with constructing a Url (e.g., Url::parse or
Url::options().base_url(...)) for the base path with the encoded job_id (keep
using self.url_encode(job_id) if you need the path encoded), then call
url.query_pairs_mut().append_pair("plan_format", fmt) when plan_format is
Some(fmt) and pass the resulting url.to_string() into self.url; this will avoid
manual string concatenation and ensure proper encoding of query values.
ballista-cli/src/tui/domain/jobs/stages.rs (1)

17-17: 💤 Low value

Clarify the purpose of file-level lint suppression.

The #![allow(unfulfilled_lint_expectations)] attribute suppresses warnings about lint expectations that were not triggered, but the rationale for adding this is unclear. If specific lints were expected but not fulfilled, it would be better to address the underlying issue or document why the suppression is necessary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/domain/jobs/stages.rs` at line 17, The file contains a
blanket crate-level attribute `#![allow(unfulfilled_lint_expectations)]` with no
explanation; remove this broad suppression and either delete it entirely or
replace it with targeted lint allowances for specific cases (or keep it but add
a short comment explaining exactly which expected_lint annotations were
intentionally left unfulfilled and why). Locate the attribute at the top of the
file (the `#![allow(unfulfilled_lint_expectations)]` line) and either remove it,
narrow it to specific lint names used in this module, or add a one-line comment
above it documenting the rationale and linking to the related lints or TODO for
resolving the underlying issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ballista-cli/src/tui/domain/jobs/stages.rs`:
- Around line 188-200: The #[allow(dead_code)] on the method set_tab should be
removed (or replaced with a justification) because set_tab is actually used;
edit the StagePlanTab::set_tab implementation to delete the attribute and run a
build/test to confirm no dead-code warning remains, or if this method is only
used under a feature flag, add the appropriate #[cfg(...)] gate or a comment
explaining the conditional usage and use #[allow(dead_code)] only within that
cfg block to justify it; reference the set_tab function and
StageDetailsView::Plan/ cached_response symbols when making the change.

In `@ballista-cli/src/tui/mod.rs`:
- Around line 358-376: In WebKeyAsyncAction::LoadStagePlan the
StagePlanTab::Default arm sets fmt to Some(""), which yields a request query
plan_format= and fails server-side deserialization; change the mapping so
Default yields None (or "default") instead of Some(""), i.e. update the match
over StagePlanTab in LoadStagePlan so fmt is None for Default before calling
get_job_stages(&job_id, fmt), keeping other arms (Tree/Metrics) as before.

In `@ballista-cli/src/tui/ui/main/jobs/mod.rs`:
- Around line 115-119: The mapping for the format parameter is inconsistent:
StagePlanTab::Default is set to Some("") while load_job_stages_popup expects
None for the no-format case; change the fmt assignment so StagePlanTab::Default
yields None (not Some("")) to match load_job_stages_popup's behavior and ensure
the HTTP client/backend receives a missing parameter rather than an empty string
(update the match that sets fmt where StagePlanTab is handled).

---

Nitpick comments:
In `@ballista-cli/src/tui/app.rs`:
- Around line 857-864: The code currently multiplexes UiData::JobDetails based
on the presence of details.physical_plan_tree, which couples routing to field
content; change to explicit variants (e.g., UiData::JobDetailsTree and
UiData::JobDetailsDefault) and update the match arm currently handling
UiData::JobDetails to handle the new variants: have UiData::JobDetailsTree carry
the details intended for job_plan_popup and assign
popup.details.physical_plan_tree = details.physical_plan_tree in that arm (using
self.job_plan_popup), and have UiData::JobDetailsDefault set self.job_details =
Some(details); also update any producers/emitters that create UiData::JobDetails
to emit the correct new variant so consumers like the match in app.rs no longer
inspect physical_plan_tree for routing.

In `@ballista-cli/src/tui/domain/jobs/stages.rs`:
- Line 17: The file contains a blanket crate-level attribute
`#![allow(unfulfilled_lint_expectations)]` with no explanation; remove this
broad suppression and either delete it entirely or replace it with targeted lint
allowances for specific cases (or keep it but add a short comment explaining
exactly which expected_lint annotations were intentionally left unfulfilled and
why). Locate the attribute at the top of the file (the
`#![allow(unfulfilled_lint_expectations)]` line) and either remove it, narrow it
to specific lint names used in this module, or add a one-line comment above it
documenting the rationale and linking to the related lints or TODO for resolving
the underlying issues.

In `@ballista-cli/src/tui/http_client.rs`:
- Around line 118-125: The code builds the job URL by string-concatenating the
query string; instead use a URL builder so query parameters are added safely.
Replace the manual formatting in the call to self.url(...) for job/{...} with
constructing a Url (e.g., Url::parse or Url::options().base_url(...)) for the
base path with the encoded job_id (keep using self.url_encode(job_id) if you
need the path encoded), then call
url.query_pairs_mut().append_pair("plan_format", fmt) when plan_format is
Some(fmt) and pass the resulting url.to_string() into self.url; this will avoid
manual string concatenation and ensure proper encoding of query values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba785880-9ee0-4bdb-987f-6cd1b070e247

📥 Commits

Reviewing files that changed from the base of the PR and between b76c1b3 and 5c57311.

📒 Files selected for processing (16)
  • .cursor/rules.md
  • .gemini/rules.md
  • AGENTS.md
  • CLAUDE.md
  • ballista-cli/src/tui/app.rs
  • ballista-cli/src/tui/domain/jobs.rs
  • ballista-cli/src/tui/domain/jobs/stages.rs
  • ballista-cli/src/tui/event.rs
  • ballista-cli/src/tui/http_client.rs
  • ballista-cli/src/tui/infrastructure/config.rs
  • ballista-cli/src/tui/mod.rs
  • ballista-cli/src/tui/ui/footer.rs
  • ballista-cli/src/tui/ui/main/jobs/job_plan_popup.rs
  • ballista-cli/src/tui/ui/main/jobs/mod.rs
  • ballista-cli/src/tui/ui/main/mod.rs
  • ballista-cli/src/tui/ui/mod.rs
💤 Files with no reviewable changes (1)
  • ballista-cli/src/tui/infrastructure/config.rs

Comment on lines +188 to 200
#[allow(dead_code)]
pub fn set_tab(&mut self, tab: StagePlanTab) -> Option<StagePlanTab> {
self.details_view = StageDetailsView::Plan(tab.clone());
self.plan_vertical_scroll_position = 0;
self.plan_horizontal_scroll_position = 0;

if let Some(cached) = self.cached_response(&tab) {
self.stages = cached.clone();
None
} else {
Some(tab)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove or justify the #[allow(dead_code)] attribute.

The set_tab method is marked with #[allow(dead_code)], but it appears to be called in app.rs (around line 232 with popup.set_tab(StagePlanTab::Default)). If the method is truly unused in certain build configurations, consider adding a feature-gate comment to clarify; otherwise, remove the attribute.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/domain/jobs/stages.rs` around lines 188 - 200, The
#[allow(dead_code)] on the method set_tab should be removed (or replaced with a
justification) because set_tab is actually used; edit the StagePlanTab::set_tab
implementation to delete the attribute and run a build/test to confirm no
dead-code warning remains, or if this method is only used under a feature flag,
add the appropriate #[cfg(...)] gate or a comment explaining the conditional
usage and use #[allow(dead_code)] only within that cfg block to justify it;
reference the set_tab function and StageDetailsView::Plan/ cached_response
symbols when making the change.

Comment on lines +358 to +376
WebKeyAsyncAction::LoadStagePlan(job_id, tab) => {
use crate::tui::domain::jobs::stages::StagePlanTab;
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};
match http_client.get_job_stages(&job_id, fmt).await {
Ok(mut stages) => {
stages
.stages
.sort_by_key(|s| s.id.parse::<u64>().unwrap_or(u64::MAX));
send_data(UiData::JobStagesPlanData(tab, stages), tx).await;
}
Err(e) => {
tracing::error!("Failed to load stage plan for '{job_id}': {e:?}")
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for server-side handling of plan_format parameter to verify empty string behavior

# Search for plan_format parameter handling in server code
rg -n --type rust "plan_format" -A 5 -B 5 -g '!ballista-cli/**'

Repository: martin-augment/datafusion-ballista

Length of output: 5955


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find the client implementation of get_job_stages
rg -n --type rust "fn get_job_stages|get_job_stages\\(" ballista-cli/src -S

# 2) Find the server endpoint/handler for "job stages plan" and its query params
rg -n --type rust "job.*stages|stages.*plan|get_job_stages|JobStages" ballista/scheduler/src/api -S

# 3) Search for the exact query parameter name used by that endpoint
rg -n --type rust "plan_format|format.*plan|stage.*plan.*format" ballista/scheduler/src/api -S

Repository: martin-augment/datafusion-ballista

Length of output: 3322


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show client request construction for get_job_stages
sed -n '120,190p' ballista-cli/src/tui/http_client.rs

# Show server route + handler around the Job stages endpoint
sed -n '1,120p' ballista/scheduler/src/api/routes.rs
sed -n '300,470p' ballista/scheduler/src/api/handlers.rs

Repository: martin-augment/datafusion-ballista

Length of output: 11935


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Client: how get_job_stages turns fmt into query params (especially when fmt == Some(""))
sed -n '120,190p' ballista-cli/src/tui/http_client.rs

# Server: handler for "/api/job/{job_id}/stages" (how query.plan_format is deserialized/handled)
sed -n '1,120p' ballista/scheduler/src/api/routes.rs
sed -n '300,470p' ballista/scheduler/src/api/handlers.rs

Repository: martin-augment/datafusion-ballista

Length of output: 11935


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the handler for /api/job/{job_id}/stages and inspect surrounding code
rg -n --type rust "get_query_stages|get_job_stages|JobStages|stages.*plan_format|plan_format.*stages" ballista/scheduler/src/api/handlers.rs -S

# Print the relevant sections around get_query_stages and PlanFormat/JobQueryParams definitions
rg -n --type rust "pub async fn get_query_stages" ballista/scheduler/src/api/handlers.rs -n
sed -n '150,260p' ballista/scheduler/src/api/handlers.rs
sed -n '260,420p' ballista/scheduler/src/api/handlers.rs
sed -n '420,650p' ballista/scheduler/src/api/handlers.rs

Repository: martin-augment/datafusion-ballista

Length of output: 19635


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '60,150p' ballista-cli/src/tui/ui/main/jobs/mod.rs
sed -n '250,430p' ballista-cli/src/tui/mod.rs

Repository: martin-augment/datafusion-ballista

Length of output: 8593


Fix default plan_format handling ("" -> invalid query value)

In LoadStagePlan, StagePlanTab::Default maps to Some(""), which get_job_stages turns into the request ...?plan_format=. The scheduler’s /api/job/{job_id}/stages handler deserializes plan_format as Option<PlanFormat> where valid values are default/tree/metrics; an empty string won’t match any variant, so the request fails instead of falling back via unwrap_or_default().
Change default to pass None (matching other call sites) or use "default" explicitly (e.g., StagePlanTab::Default => None).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/mod.rs` around lines 358 - 376, In
WebKeyAsyncAction::LoadStagePlan the StagePlanTab::Default arm sets fmt to
Some(""), which yields a request query plan_format= and fails server-side
deserialization; change the mapping so Default yields None (or "default")
instead of Some(""), i.e. update the match over StagePlanTab in LoadStagePlan so
fmt is None for Default before calling get_job_stages(&job_id, fmt), keeping
other arms (Tree/Metrics) as before.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! Instead of passing an Option<&str> it would be better to pass &StagePlanTab and add implementations to it to represent itself as a query parameter and as String, i.e. a Display impl. This way the APIs will be typed and adding a new enum variant will be done in one place - StagePlanTab and its impls, not all over the codebase.

Comment on lines +115 to +119
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent format mapping for default case.

StagePlanTab::Default maps to Some(""), but load_job_stages_popup (line 89) passes None for the default/no-format case. If the HTTP client or backend treats an empty string differently from a missing parameter, this could produce inconsistent results.

🔧 Proposed fix for consistency
     let fmt = match tab {
-        StagePlanTab::Default => Some(""),
+        StagePlanTab::Default => None,
         StagePlanTab::Tree => Some("tree"),
         StagePlanTab::Metrics => Some("metrics"),
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let fmt = match tab {
StagePlanTab::Default => Some(""),
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};
let fmt = match tab {
StagePlanTab::Default => None,
StagePlanTab::Tree => Some("tree"),
StagePlanTab::Metrics => Some("metrics"),
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ballista-cli/src/tui/ui/main/jobs/mod.rs` around lines 115 - 119, The mapping
for the format parameter is inconsistent: StagePlanTab::Default is set to
Some("") while load_job_stages_popup expects None for the no-format case; change
the fmt assignment so StagePlanTab::Default yields None (not Some("")) to match
load_job_stages_popup's behavior and ensure the HTTP client/backend receives a
missing parameter rather than an empty string (update the match that sets fmt
where StagePlanTab is handled).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value:useful; category:bug; feedback: The CodeRabbit AI reviewer is correct! Instead of passing an Option<&str> it would be better to pass &StagePlanTab and add implementations to it to represent itself as a query parameter and as String, i.e. a Display impl. This way the APIs will be typed and adding a new enum variant will be done in one place - StagePlanTab and its impls, not all over the codebase.

@martin-augment

Copy link
Copy Markdown
Owner Author

1. StagePlanTab::Default maps to Some("") instead of None

In both ballista-cli/src/tui/ui/main/jobs/mod.rs (load_stage_plan) and ballista-cli/src/tui/mod.rs (LoadStagePlan handler):

StagePlanTab::Default => Some(""),

This produces ?plan_format= (empty value) in the request URL instead of omitting the parameter entirely. The server likely won't treat an empty plan_format the same as no plan_format. This should be None to match the original behaviour for the default format.

value:useful; category:bug; feedback: The Claude AI reviewer is correct! Instead of passing an Option<&str> it would be better to pass &StagePlanTab and add implementations to it to represent itself as a query parameter and as String, i.e. a Display impl. This way the APIs will be typed and adding a new enum variant will be done in one place - StagePlanTab and its impls, not all over the codebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants