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