Skip to content

Commit 0f467d6

Browse files
committed
fix: WIP create tui claim generator
1 parent 1bd1306 commit 0f467d6

File tree

14 files changed

+1769
-5
lines changed

14 files changed

+1769
-5
lines changed

cli/src/tui/app.rs

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use anyhow::Result;
22

33
use super::state::{
4-
detail_state::DetailState, events_state::EventsState, modal_state::ModalState,
5-
search_state::SearchState, view_state::ViewState,
4+
claim_builder_state::ClaimBuilderState, detail_state::DetailState, events_state::EventsState,
5+
modal_state::ModalState, search_state::SearchState, view_state::ViewState,
66
};
77
use super::utils::NavItem;
88
use crate::current_region_handler;
@@ -35,6 +35,8 @@ pub enum PendingAction {
3535
LoadJobLogs(String),
3636
ReapplyDeployment(usize),
3737
DestroyDeployment(usize),
38+
SaveClaimToFile,
39+
RunClaimFromBuilder,
3840
}
3941

4042
#[derive(Debug, Clone)]
@@ -100,6 +102,7 @@ pub struct App {
100102
pub events_state: EventsState,
101103
pub modal_state: ModalState,
102104
pub search_state: SearchState,
105+
pub claim_builder_state: ClaimBuilderState,
103106

104107
// ==================== LEGACY FIELDS (TRANSITIONING) ====================
105108
// These are kept for backward compatibility during migration.
@@ -167,6 +170,7 @@ impl App {
167170
let events_state = EventsState::new();
168171
let modal_state = ModalState::new();
169172
let search_state = SearchState::new();
173+
let claim_builder_state = ClaimBuilderState::new();
170174

171175
Self {
172176
// Core app state
@@ -189,6 +193,7 @@ impl App {
189193
events_state,
190194
modal_state,
191195
search_state,
196+
claim_builder_state,
192197

193198
// Legacy fields - initialized from defaults for backward compatibility
194199
// View state
@@ -585,6 +590,12 @@ impl App {
585590
self.selected_index = index;
586591
self.destroy_deployment().await?;
587592
}
593+
PendingAction::SaveClaimToFile => {
594+
self.save_claim_to_file().await?;
595+
}
596+
PendingAction::RunClaimFromBuilder => {
597+
self.run_claim_from_builder().await?;
598+
}
588599
}
589600

590601
Ok(())
@@ -640,6 +651,12 @@ impl App {
640651
PendingAction::DestroyDeployment(_) => {
641652
self.set_loading("Destroying deployment...");
642653
}
654+
PendingAction::SaveClaimToFile => {
655+
self.set_loading("Saving claim to file...");
656+
}
657+
PendingAction::RunClaimFromBuilder => {
658+
self.set_loading("Running claim...");
659+
}
643660
}
644661
}
645662

@@ -2032,6 +2049,129 @@ impl App {
20322049
}
20332050
self.close_confirmation();
20342051
}
2052+
2053+
/// Save the claim builder's generated YAML to a file
2054+
pub async fn save_claim_to_file(&mut self) -> Result<()> {
2055+
use std::fs;
2056+
use std::path::PathBuf;
2057+
2058+
// Validate the form first
2059+
if let Err(err) = self.claim_builder_state.validate() {
2060+
self.detail_state
2061+
.show_error(&format!("Validation failed: {}", err));
2062+
self.clear_loading();
2063+
return Ok(());
2064+
}
2065+
2066+
// Generate the YAML
2067+
self.claim_builder_state.generate_yaml();
2068+
2069+
// Create a filename based on deployment name
2070+
let deployment_name = if self.claim_builder_state.deployment_name.is_empty() {
2071+
"deployment".to_string()
2072+
} else {
2073+
self.claim_builder_state.deployment_name.clone()
2074+
};
2075+
2076+
let filename = format!("{}.yaml", deployment_name);
2077+
let mut filepath = PathBuf::from("./");
2078+
filepath.push(&filename);
2079+
2080+
// Write to file
2081+
match fs::write(&filepath, &self.claim_builder_state.generated_yaml) {
2082+
Ok(_) => {
2083+
self.detail_state
2084+
.show_message(format!("Claim saved to: {}", filepath.display()));
2085+
// Close the claim builder after successful save
2086+
self.claim_builder_state.close();
2087+
}
2088+
Err(e) => {
2089+
self.detail_state
2090+
.show_error(&format!("Failed to save claim: {}", e));
2091+
}
2092+
}
2093+
2094+
self.clear_loading();
2095+
Ok(())
2096+
}
2097+
2098+
pub async fn run_claim_from_builder(&mut self) -> Result<()> {
2099+
use crate::utils::get_environment;
2100+
use env_common::logic::run_claim;
2101+
use env_defs::ExtraData;
2102+
2103+
// Validate the form first
2104+
if let Err(err) = self.claim_builder_state.validate() {
2105+
self.detail_state
2106+
.show_error(&format!("Validation failed: {}", err));
2107+
self.clear_loading();
2108+
return Ok(());
2109+
}
2110+
2111+
// Generate the YAML
2112+
self.claim_builder_state.generate_yaml();
2113+
2114+
// Parse the claim YAML
2115+
let yaml: serde_yaml::Value =
2116+
match serde_yaml::from_str(&self.claim_builder_state.generated_yaml) {
2117+
Ok(y) => y,
2118+
Err(e) => {
2119+
self.detail_state
2120+
.show_error(&format!("Failed to parse YAML: {}", e));
2121+
self.clear_loading();
2122+
return Ok(());
2123+
}
2124+
};
2125+
2126+
// Use the same environment handling as the CLI
2127+
let environment = get_environment("default");
2128+
2129+
let reference_fallback = match hostname::get() {
2130+
Ok(hostname) => hostname.to_string_lossy().to_string(),
2131+
Err(e) => {
2132+
self.detail_state
2133+
.show_error(&format!("Failed to get hostname: {}", e));
2134+
self.clear_loading();
2135+
return Ok(());
2136+
}
2137+
};
2138+
2139+
// Run the claim using current_region_handler (same as reapply_deployment)
2140+
let handler = current_region_handler().await;
2141+
match run_claim(
2142+
&handler,
2143+
&yaml,
2144+
&environment,
2145+
"apply",
2146+
vec![],
2147+
ExtraData::None,
2148+
&reference_fallback,
2149+
)
2150+
.await
2151+
{
2152+
Ok((job_id, deployment_id, _)) => {
2153+
let message = format!(
2154+
"✅ Claim applied successfully!\n\nJob ID: {}\nDeployment ID: {}\nEnvironment: {}",
2155+
job_id, deployment_id, environment
2156+
);
2157+
2158+
self.detail_state.show_message(message.clone());
2159+
2160+
// Close the claim builder after successful run
2161+
self.claim_builder_state.close();
2162+
2163+
// Reload deployments list
2164+
self.schedule_action(PendingAction::LoadDeployments);
2165+
}
2166+
Err(e) => {
2167+
let message = format!("❌ Failed to run claim:\n\n{}", e);
2168+
self.detail_state.show_message(message);
2169+
}
2170+
}
2171+
2172+
self.clear_loading();
2173+
Ok(())
2174+
}
20352175
}
20362176

20372177
// Implement VersionItem trait for Module to work with VersionsModal widget
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use anyhow::Result;
2+
use crossterm::event::{KeyCode, KeyModifiers};
3+
4+
use crate::tui::app::App;
5+
6+
pub struct ClaimBuilderHandler;
7+
8+
impl ClaimBuilderHandler {
9+
pub fn handle_key(app: &mut App, key: KeyCode, modifiers: KeyModifiers) -> Result<()> {
10+
let state = &mut app.claim_builder_state;
11+
12+
// Handle Ctrl key combinations first
13+
if modifiers.contains(KeyModifiers::CONTROL) {
14+
match key {
15+
KeyCode::Char('p') => {
16+
state.toggle_preview();
17+
return Ok(());
18+
}
19+
KeyCode::Char('s') => {
20+
// Save to file
21+
if state.show_preview {
22+
app.schedule_action(crate::tui::app::PendingAction::SaveClaimToFile);
23+
} else {
24+
state.generate_yaml();
25+
app.schedule_action(crate::tui::app::PendingAction::SaveClaimToFile);
26+
}
27+
return Ok(());
28+
}
29+
KeyCode::Char('r') => {
30+
// Run claim
31+
if state.show_preview {
32+
app.schedule_action(crate::tui::app::PendingAction::RunClaimFromBuilder);
33+
} else {
34+
state.generate_yaml();
35+
app.schedule_action(crate::tui::app::PendingAction::RunClaimFromBuilder);
36+
}
37+
return Ok(());
38+
}
39+
KeyCode::Char('t') => {
40+
// Insert template for current field type
41+
if !state.show_preview {
42+
state.insert_template();
43+
}
44+
return Ok(());
45+
}
46+
_ => {}
47+
}
48+
}
49+
50+
if state.show_preview {
51+
// Preview mode navigation
52+
match key {
53+
KeyCode::Up => {
54+
state.scroll_preview_up();
55+
}
56+
KeyCode::Down => {
57+
state.scroll_preview_down();
58+
}
59+
KeyCode::PageUp => {
60+
for _ in 0..10 {
61+
state.scroll_preview_up();
62+
}
63+
}
64+
KeyCode::PageDown => {
65+
for _ in 0..10 {
66+
state.scroll_preview_down();
67+
}
68+
}
69+
KeyCode::Esc => {
70+
// Go back to form editing instead of closing
71+
state.toggle_preview();
72+
}
73+
_ => {}
74+
}
75+
} else {
76+
// Form editing mode
77+
match key {
78+
KeyCode::Tab => {
79+
state.next_field();
80+
}
81+
KeyCode::BackTab => {
82+
state.previous_field();
83+
}
84+
KeyCode::Up => {
85+
state.previous_field();
86+
}
87+
KeyCode::Down => {
88+
state.next_field();
89+
}
90+
KeyCode::Left => {
91+
state.move_cursor_left();
92+
}
93+
KeyCode::Right => {
94+
state.move_cursor_right();
95+
}
96+
KeyCode::Home => match state.selected_field_index {
97+
0 => state.deployment_name_cursor = 0,
98+
i if i >= 1 => {
99+
let var_index = i - 1;
100+
if let Some(input) = state.variable_inputs.get_mut(var_index) {
101+
input.move_cursor_home();
102+
}
103+
}
104+
_ => {}
105+
},
106+
KeyCode::End => match state.selected_field_index {
107+
0 => state.deployment_name_cursor = state.deployment_name.len(),
108+
i if i >= 1 => {
109+
let var_index = i - 1;
110+
if let Some(input) = state.variable_inputs.get_mut(var_index) {
111+
input.move_cursor_end();
112+
}
113+
}
114+
_ => {}
115+
},
116+
KeyCode::Backspace => {
117+
state.backspace();
118+
}
119+
KeyCode::Enter => {
120+
// Toggle YAML preview
121+
state.toggle_preview();
122+
}
123+
KeyCode::Char(c) => {
124+
state.insert_char(c);
125+
}
126+
KeyCode::Esc => {
127+
state.close();
128+
}
129+
_ => {}
130+
}
131+
}
132+
133+
Ok(())
134+
}
135+
}

cli/src/tui/events/detail_handler.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ impl DetailHandler {
1111
KeyCode::Esc | KeyCode::Char('q') => {
1212
app.close_detail();
1313
}
14+
KeyCode::Char('c') => {
15+
// Open claim builder
16+
if let Some(module) = app.detail_state.detail_module.clone() {
17+
app.claim_builder_state.open_for_module(module);
18+
} else if let Some(stack) = app.detail_state.detail_stack.clone() {
19+
app.claim_builder_state.open_for_stack(stack);
20+
}
21+
}
1422
KeyCode::Char('h') | KeyCode::Left => {
1523
app.detail_focus_left();
1624
}

cli/src/tui/events/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
pub mod claim_builder_handler;
12
pub mod detail_handler;
23
pub mod events_handler;
34
pub mod main_handler;
45
pub mod modal_handler;
56

7+
pub use claim_builder_handler::ClaimBuilderHandler;
68
pub use detail_handler::DetailHandler;
79
pub use events_handler::EventsHandler;
810
pub use main_handler::MainHandler;

cli/src/tui/handlers.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
33
use std::time::Duration;
44

55
use super::app::App;
6-
use super::events::{DetailHandler, EventsHandler, MainHandler, ModalHandler};
6+
use super::events::{ClaimBuilderHandler, DetailHandler, EventsHandler, MainHandler, ModalHandler};
77

88
pub async fn handle_events(app: &mut App) -> Result<()> {
99
if event::poll(Duration::from_millis(100))? {
@@ -33,6 +33,10 @@ fn handle_key_event(app: &mut App, key: KeyCode, modifiers: KeyModifiers) -> Res
3333
return ModalHandler::handle_versions_key(app, key);
3434
}
3535

36+
if app.claim_builder_state.showing_claim_builder {
37+
return ClaimBuilderHandler::handle_key(app, key, modifiers);
38+
}
39+
3640
if app.events_state.showing_events {
3741
return EventsHandler::handle_key(app, key);
3842
}

cli/src/tui/renderers/common.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ fn get_footer_actions(app: &App) -> Vec<(&'static str, &'static str)> {
196196
}
197197
}
198198

199+
shortcuts.push(("c", "Build Claim"));
199200
shortcuts.push(("ESC/q", "Close"));
200201
shortcuts.push(("Ctrl+C", "Quit"));
201202

cli/src/tui/renderers/modules_renderer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ pub fn render_modules(frame: &mut Frame, area: Rect, app: &App) {
4949
let headers = vec!["Module Name", "Stable", "RC", "Beta", "Alpha", "Dev"];
5050
let widths = vec![30, 10, 10, 15, 15, 20];
5151

52-
let widget = TableWidget::new("📦 Modules", "📦", headers, widths)
52+
let widget = TableWidget::new("Modules", "📦", headers, widths)
5353
.with_rows(rows)
5454
.with_selected(app.selected_index);
5555

0 commit comments

Comments
 (0)