node_launchpad/components/popup/
manage_nodes.rs1use std::path::PathBuf;
10
11use crate::action::OptionsActions;
12use crate::system::get_available_space_b;
13use color_eyre::Result;
14use crossterm::event::{Event, KeyCode, KeyEvent};
15use ratatui::{prelude::*, widgets::*};
16use tui_input::{backend::crossterm::EventHandler, Input};
17
18use crate::{
19 action::Action,
20 mode::{InputMode, Scene},
21 style::{clear_area, EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VIVID_SKY_BLUE},
22};
23
24use super::super::{utils::centered_rect_fixed, Component};
25
26pub const GB_PER_NODE: usize = 35;
27pub const MB: usize = 1000 * 1000;
28pub const GB: usize = MB * 1000;
29pub const MAX_NODE_COUNT: usize = 50;
30
31pub struct ManageNodes {
32 active: bool,
34 available_disk_space_gb: usize,
35 storage_mountpoint: PathBuf,
36 nodes_to_start_input: Input,
37 old_value: String,
39}
40
41impl ManageNodes {
42 pub fn new(nodes_to_start: usize, storage_mountpoint: PathBuf) -> Result<Self> {
43 let nodes_to_start = std::cmp::min(nodes_to_start, MAX_NODE_COUNT);
44 let new = Self {
45 active: false,
46 available_disk_space_gb: get_available_space_b(&storage_mountpoint)? / GB,
47 nodes_to_start_input: Input::default().with_value(nodes_to_start.to_string()),
48 old_value: Default::default(),
49 storage_mountpoint: storage_mountpoint.clone(),
50 };
51 Ok(new)
52 }
53
54 fn get_nodes_to_start_val(&self) -> usize {
55 self.nodes_to_start_input.value().parse().unwrap_or(0)
56 }
57
58 fn max_nodes_to_start(&self) -> usize {
61 std::cmp::min(self.available_disk_space_gb / GB_PER_NODE, MAX_NODE_COUNT)
62 }
63}
64
65impl Component for ManageNodes {
66 fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
67 if !self.active {
68 return Ok(vec![]);
69 }
70
71 let send_back = match key.code {
73 KeyCode::Enter => {
74 let nodes_to_start_str = self.nodes_to_start_input.value().to_string();
75 let nodes_to_start =
76 std::cmp::min(self.get_nodes_to_start_val(), self.max_nodes_to_start());
77
78 self.nodes_to_start_input = self
80 .nodes_to_start_input
81 .clone()
82 .with_value(nodes_to_start.to_string());
83
84 debug!(
85 "Got Enter, value found to be {nodes_to_start} derived from input: {nodes_to_start_str:?} and switching scene",
86 );
87 vec![
88 Action::StoreNodesToStart(nodes_to_start),
89 Action::SwitchScene(Scene::Status),
90 ]
91 }
92 KeyCode::Esc => {
93 debug!(
94 "Got Esc, restoring the old value {} and switching to home",
95 self.old_value
96 );
97 self.nodes_to_start_input = self
99 .nodes_to_start_input
100 .clone()
101 .with_value(self.old_value.clone());
102 vec![Action::SwitchScene(Scene::Status)]
103 }
104 KeyCode::Char(c) if c.is_numeric() => {
105 if c == '0' && self.nodes_to_start_input.value().is_empty() {
107 return Ok(vec![]);
108 }
109 let number = c.to_string().parse::<usize>().unwrap_or(0);
110 let new_value = format!("{}{}", self.get_nodes_to_start_val(), number)
111 .parse::<usize>()
112 .unwrap_or(0);
113 if new_value * GB_PER_NODE > self.available_disk_space_gb
115 || new_value > MAX_NODE_COUNT
116 {
117 self.nodes_to_start_input = self
118 .nodes_to_start_input
119 .clone()
120 .with_value(self.max_nodes_to_start().to_string());
121 return Ok(vec![]);
122 }
123 self.nodes_to_start_input.handle_event(&Event::Key(key));
124 vec![]
125 }
126 KeyCode::Backspace => {
127 self.nodes_to_start_input.handle_event(&Event::Key(key));
128 vec![]
129 }
130 KeyCode::Up | KeyCode::Down => {
131 let nodes_to_start = {
132 let current_val = self.get_nodes_to_start_val();
133
134 if key.code == KeyCode::Up {
135 if current_val + 1 >= MAX_NODE_COUNT {
136 MAX_NODE_COUNT
137 } else if (current_val + 1) * GB_PER_NODE <= self.available_disk_space_gb {
138 current_val + 1
139 } else {
140 current_val
141 }
142 } else {
143 if current_val == 0 {
145 0
146 } else {
147 current_val - 1
148 }
149 }
150 };
151 self.nodes_to_start_input = self
153 .nodes_to_start_input
154 .clone()
155 .with_value(nodes_to_start.to_string());
156 vec![]
157 }
158 _ => {
159 vec![]
160 }
161 };
162 Ok(send_back)
163 }
164
165 fn update(&mut self, action: Action) -> Result<Option<Action>> {
166 let send_back = match action {
167 Action::SwitchScene(scene) => match scene {
168 Scene::ManageNodesPopUp { amount_of_nodes } => {
169 self.nodes_to_start_input = self
170 .nodes_to_start_input
171 .clone()
172 .with_value(amount_of_nodes.to_string());
173 self.active = true;
174 self.old_value = self.nodes_to_start_input.value().to_string();
175 Some(Action::SwitchInputMode(InputMode::Entry))
178 }
179 _ => {
180 self.active = false;
181 None
182 }
183 },
184 Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, _drive_name)) => {
185 self.storage_mountpoint.clone_from(&mountpoint);
186 self.available_disk_space_gb = get_available_space_b(&mountpoint)? / GB;
187 None
188 }
189 _ => None,
190 };
191 Ok(send_back)
192 }
193
194 fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
195 if !self.active {
196 return Ok(());
197 }
198
199 let layer_zero = centered_rect_fixed(52, 15, area);
200 let layer_one = Layout::new(
201 Direction::Vertical,
202 [
203 Constraint::Length(2),
205 Constraint::Length(1),
207 Constraint::Length(1),
209 Constraint::Length(1),
211 Constraint::Length(7),
213 Constraint::Min(1),
215 Constraint::Length(1),
217 Constraint::Length(1),
219 ],
220 )
221 .split(layer_zero);
222 let pop_up_border = Paragraph::new("").block(
223 Block::default()
224 .borders(Borders::ALL)
225 .title(" Manage Nodes ")
226 .bold()
227 .title_style(Style::new().fg(GHOST_WHITE))
228 .title_style(Style::new().fg(EUCALYPTUS))
229 .padding(Padding::uniform(2))
230 .border_style(Style::new().fg(EUCALYPTUS)),
231 );
232 clear_area(f, layer_zero);
233
234 let layer_input_field = Layout::new(
236 Direction::Horizontal,
237 [
238 Constraint::Min(5),
240 Constraint::Length(5),
242 Constraint::Length(5),
244 Constraint::Length(8),
246 Constraint::Min(5),
248 ],
249 )
250 .split(layer_one[1]);
251
252 let start = Paragraph::new("Start ").style(Style::default().fg(GHOST_WHITE));
253 f.render_widget(start, layer_input_field[1]);
254
255 let width = layer_input_field[2].width.max(3) - 3;
256 let scroll = self.nodes_to_start_input.visual_scroll(width as usize);
257 let input = Paragraph::new(self.get_nodes_to_start_val().to_string())
258 .style(Style::new().fg(VIVID_SKY_BLUE))
259 .scroll((0, scroll as u16))
260 .alignment(Alignment::Center);
261
262 f.render_widget(input, layer_input_field[2]);
263
264 let nodes_text = Paragraph::new("Node(s)").fg(GHOST_WHITE);
265 f.render_widget(nodes_text, layer_input_field[3]);
266
267 let available_space_gb = self.available_disk_space_gb;
269 let info_style = Style::default().fg(VIVID_SKY_BLUE);
270 let info = Line::from(vec![
271 Span::styled("Using", info_style),
272 Span::styled(
273 format!(" {}GB ", self.get_nodes_to_start_val() * GB_PER_NODE),
274 info_style.bold(),
275 ),
276 Span::styled(
277 format!("of {available_space_gb}GB available space"),
278 info_style,
279 ),
280 ]);
281 let info = Paragraph::new(info).alignment(Alignment::Center);
282 f.render_widget(info, layer_one[2]);
283
284 let help = Paragraph::new(vec![
286 Line::raw(format!(
287 "Note: Each node will use a small amount of CPU Memory and Network Bandwidth. \
288 We recommend starting no more than 2 at a time (max {MAX_NODE_COUNT} nodes)."
289 )),
290 Line::raw(""),
291 Line::raw("▲▼ to change the number of nodes to start."),
292 ])
293 .wrap(Wrap { trim: false })
294 .block(Block::default().padding(Padding::horizontal(4)))
295 .alignment(Alignment::Center)
296 .fg(GHOST_WHITE);
297 f.render_widget(help, layer_one[4]);
298
299 let dash = Block::new()
301 .borders(Borders::BOTTOM)
302 .border_style(Style::new().fg(GHOST_WHITE));
303 f.render_widget(dash, layer_one[5]);
304
305 let buttons_layer =
307 Layout::horizontal(vec![Constraint::Percentage(45), Constraint::Percentage(55)])
308 .split(layer_one[6]);
309
310 let button_no = Line::from(vec![Span::styled(
311 " Close [Esc]",
312 Style::default().fg(LIGHT_PERIWINKLE),
313 )]);
314 f.render_widget(button_no, buttons_layer[0]);
315 let button_yes = Line::from(vec![Span::styled(
316 "Start Node(s) [Enter] ",
317 Style::default().fg(EUCALYPTUS),
318 )]);
319 let button_yes = Paragraph::new(button_yes).alignment(Alignment::Right);
320 f.render_widget(button_yes, buttons_layer[1]);
321
322 f.render_widget(pop_up_border, layer_zero);
323
324 Ok(())
325 }
326}