1use std::{default::Default, path::PathBuf, rc::Rc};
10
11use super::super::utils::centered_rect_fixed;
12
13use color_eyre::Result;
14use crossterm::event::{KeyCode, KeyEvent};
15use ratatui::{
16 layout::{Alignment, Constraint, Direction, Layout, Rect},
17 style::{Style, Stylize},
18 text::{Line, Span},
19 widgets::{
20 Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, Wrap,
21 },
22};
23
24use crate::{
25 action::{Action, OptionsActions},
26 components::{
27 Component,
28 popup::manage_nodes::{GB, GB_PER_NODE},
29 },
30 config::get_launchpad_nodes_data_dir_path,
31 mode::{InputMode, Scene},
32 style::{
33 COOL_GREY, DARK_GUNMETAL, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE,
34 VIVID_SKY_BLUE, clear_area,
35 },
36 system,
37};
38
39#[derive(Default)]
40enum ChangeDriveState {
41 #[default]
42 Selection,
43 ConfirmChange,
44}
45
46#[derive(Default)]
47pub struct ChangeDrivePopup {
48 active: bool,
49 state: ChangeDriveState,
50 items: Option<StatefulList<DriveItem>>,
51 drive_selection: DriveItem,
52 drive_selection_initial_state: DriveItem,
53 nodes_to_start: usize,
54 storage_mountpoint: PathBuf,
55 can_select: bool, }
57
58impl ChangeDrivePopup {
59 pub fn new(storage_mountpoint: PathBuf, nodes_to_start: usize) -> Result<Self> {
60 debug!("Drive Mountpoint in Config: {:?}", storage_mountpoint);
61 Ok(ChangeDrivePopup {
62 active: false,
63 state: ChangeDriveState::Selection,
64 items: None,
65 drive_selection: DriveItem::default(),
66 drive_selection_initial_state: DriveItem::default(),
67 nodes_to_start,
68 storage_mountpoint,
69 can_select: false,
70 })
71 }
72
73 fn deselect_all(&mut self) {
78 if let Some(ref mut items) = self.items {
79 for item in &mut items.items {
80 if item.status != DriveStatus::NotAvailable
81 && item.status != DriveStatus::NotEnoughSpace
82 {
83 item.status = DriveStatus::NotSelected;
84 }
85 }
86 }
87 }
88 fn assign_drive_selection(&mut self) {
91 self.deselect_all();
92 if let Some(ref mut items) = self.items
93 && let Some(i) = items.state.selected()
94 {
95 items.items[i].status = DriveStatus::Selected;
96 self.drive_selection = items.items[i].clone();
97 }
98 }
99 fn select_drive(&mut self) {
102 self.deselect_all();
103 if let Some(ref mut items) = self.items {
104 for (index, item) in items.items.iter_mut().enumerate() {
105 if item.mountpoint == self.drive_selection.mountpoint {
106 item.status = DriveStatus::Selected;
107 items.state.select(Some(index));
108 break;
109 }
110 }
111 }
112 }
113 fn return_selection(&mut self) -> DriveItem {
116 if let Some(ref mut items) = self.items
117 && let Some(i) = items.state.selected()
118 {
119 return items.items[i].clone();
120 }
121 DriveItem::default()
122 }
123
124 fn update_drive_items(&mut self) -> Result<()> {
126 let drives_and_space = system::get_list_of_available_drives_and_available_space()?;
127 let drives_items: Vec<DriveItem> = drives_and_space
128 .iter()
129 .map(|(drive_name, mountpoint, space, available)| {
130 let size_str = format!("{:.2} GB", *space as f64 / 1e9);
131 let has_enough_space = *space as u128
132 >= (GB_PER_NODE as u128 * GB as u128 * self.nodes_to_start as u128);
133 DriveItem {
134 name: drive_name.to_string(),
135 mountpoint: mountpoint.clone(),
136 size: size_str.clone(),
137 status: if *mountpoint == self.storage_mountpoint {
138 self.drive_selection = DriveItem {
139 name: drive_name.to_string(),
140 mountpoint: mountpoint.clone(),
141 size: size_str.clone(),
142 status: DriveStatus::Selected,
143 };
144 DriveStatus::Selected
145 } else if !available {
146 DriveStatus::NotAvailable
147 } else if !has_enough_space {
148 DriveStatus::NotEnoughSpace
149 } else {
150 DriveStatus::NotSelected
151 },
152 }
153 })
154 .collect();
155 self.items = Some(StatefulList::with_items(drives_items.clone()));
156 debug!("Drives and space: {:?}", drives_and_space);
157 debug!("Drives items: {:?}", drives_items);
158 Ok(())
159 }
160
161 fn draw_selection_state(
165 &mut self,
166 f: &mut crate::tui::Frame<'_>,
167 layer_zero: Rect,
168 layer_one: Rc<[Rect]>,
169 ) -> Paragraph<'_> {
170 let pop_up_border = Paragraph::new("").block(
171 Block::default()
172 .borders(Borders::ALL)
173 .title(" Select a Drive ")
174 .bold()
175 .title_style(Style::new().fg(VIVID_SKY_BLUE))
176 .padding(Padding::uniform(2))
177 .border_style(Style::new().fg(VIVID_SKY_BLUE)),
178 );
179 clear_area(f, layer_zero);
180
181 let layer_two = Layout::new(
182 Direction::Vertical,
183 [
184 Constraint::Length(10),
186 Constraint::Length(3),
188 Constraint::Length(1),
190 ],
191 )
192 .split(layer_one[1]);
193
194 let items: Vec<ListItem> = self
196 .items
197 .as_ref()
198 .unwrap()
199 .items
200 .iter()
201 .enumerate()
202 .map(|(i, drive_item)| drive_item.to_list_item(i, layer_two[0].width as usize))
203 .collect();
204
205 let items = List::new(items)
206 .block(Block::default().padding(Padding::uniform(1)))
207 .highlight_style(Style::default().bg(INDIGO))
208 .highlight_spacing(HighlightSpacing::Always);
209
210 f.render_stateful_widget(items, layer_two[0], &mut self.items.clone().unwrap().state);
211
212 let dash = Block::new()
214 .borders(Borders::BOTTOM)
215 .border_style(Style::new().fg(GHOST_WHITE));
216 f.render_widget(dash, layer_two[1]);
217
218 let buttons_layer =
220 Layout::horizontal(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
221 .split(layer_two[2]);
222
223 let button_no = Line::from(vec![Span::styled(
224 "Cancel [Esc]",
225 Style::default().fg(LIGHT_PERIWINKLE),
226 )]);
227
228 f.render_widget(
229 Paragraph::new(button_no)
230 .block(Block::default().padding(Padding::horizontal(2)))
231 .alignment(Alignment::Left),
232 buttons_layer[0],
233 );
234
235 let button_yes = Line::from(vec![
236 Span::styled(
237 "Change Drive ",
238 if self.can_select {
239 Style::default().fg(EUCALYPTUS)
240 } else {
241 Style::default().fg(COOL_GREY)
242 },
243 ),
244 Span::styled("[Enter]", Style::default().fg(LIGHT_PERIWINKLE).bold()),
245 ])
246 .alignment(Alignment::Right);
247
248 f.render_widget(
249 Paragraph::new(button_yes)
250 .block(Block::default().padding(Padding::horizontal(2)))
251 .alignment(Alignment::Right),
252 buttons_layer[1],
253 );
254
255 pop_up_border
256 }
257
258 fn draw_confirm_change_state(
260 &mut self,
261 f: &mut crate::tui::Frame<'_>,
262 layer_zero: Rect,
263 layer_one: Rc<[Rect]>,
264 ) -> Paragraph<'_> {
265 let pop_up_border = Paragraph::new("").block(
266 Block::default()
267 .borders(Borders::ALL)
268 .title(" Confirm & Reset ")
269 .bold()
270 .title_style(Style::new().fg(VIVID_SKY_BLUE))
271 .padding(Padding::uniform(2))
272 .border_style(Style::new().fg(VIVID_SKY_BLUE))
273 .bg(DARK_GUNMETAL),
274 );
275 clear_area(f, layer_zero);
276
277 let layer_two = Layout::new(
278 Direction::Vertical,
279 [
280 Constraint::Length(10),
282 Constraint::Length(3),
284 Constraint::Length(1),
286 ],
287 )
288 .split(layer_one[1]);
289
290 let text = vec![
292 Line::from(vec![]), Line::from(vec![]), Line::from(vec![
295 Span::styled("Changing storage to ", Style::default().fg(GHOST_WHITE)),
296 Span::styled(
297 format!("{} ", self.drive_selection.name),
298 Style::default().fg(VIVID_SKY_BLUE),
299 ),
300 Span::styled("will ", Style::default().fg(GHOST_WHITE)),
301 ])
302 .alignment(Alignment::Center),
303 Line::from(vec![Span::styled(
304 "reset all nodes.",
305 Style::default().fg(GHOST_WHITE),
306 )])
307 .alignment(Alignment::Center),
308 Line::from(vec![]), Line::from(vec![]), Line::from(vec![
311 Span::styled("You’ll need to ", Style::default().fg(GHOST_WHITE)),
312 Span::styled("Add ", Style::default().fg(GHOST_WHITE).bold()),
313 Span::styled("and ", Style::default().fg(GHOST_WHITE)),
314 Span::styled("Start ", Style::default().fg(GHOST_WHITE).bold()),
315 Span::styled(
316 "them again afterwards. Are you sure you want to continue?",
317 Style::default().fg(GHOST_WHITE),
318 ),
319 ])
320 .alignment(Alignment::Center),
321 ];
322 let paragraph = Paragraph::new(text)
323 .wrap(Wrap { trim: false })
324 .block(
325 Block::default()
326 .borders(Borders::NONE)
327 .padding(Padding::horizontal(2)),
328 )
329 .alignment(Alignment::Center)
330 .style(Style::default().fg(GHOST_WHITE).bg(DARK_GUNMETAL));
331
332 f.render_widget(paragraph, layer_two[0]);
333
334 let dash = Block::new()
336 .borders(Borders::BOTTOM)
337 .border_style(Style::new().fg(GHOST_WHITE));
338 f.render_widget(dash, layer_two[1]);
339
340 let buttons_layer =
342 Layout::horizontal(vec![Constraint::Percentage(30), Constraint::Percentage(70)])
343 .split(layer_two[2]);
344
345 let button_no = Line::from(vec![Span::styled(
346 "Back [Esc]",
347 Style::default().fg(LIGHT_PERIWINKLE),
348 )]);
349
350 f.render_widget(
351 Paragraph::new(button_no)
352 .block(Block::default().padding(Padding::horizontal(2)))
353 .alignment(Alignment::Left),
354 buttons_layer[0],
355 );
356
357 let button_yes = Line::from(vec![
358 Span::styled("Yes, change drive ", Style::default().fg(EUCALYPTUS)),
359 Span::styled("[Enter]", Style::default().fg(LIGHT_PERIWINKLE).bold()),
360 ])
361 .alignment(Alignment::Right);
362
363 f.render_widget(
364 Paragraph::new(button_yes)
365 .block(Block::default().padding(Padding::horizontal(2)))
366 .alignment(Alignment::Right),
367 buttons_layer[1],
368 );
369
370 pop_up_border
371 }
372}
373
374impl Component for ChangeDrivePopup {
375 fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
376 if !self.active {
377 return Ok(vec![]);
378 }
379 let send_back: Vec<Action> = match &self.state {
380 ChangeDriveState::Selection => {
381 match key.code {
382 KeyCode::Enter => {
383 let drive = self.return_selection();
386 if self.can_select {
387 debug!(
388 "Got Enter and there's a new selection, storing value and switching to Options"
389 );
390 debug!("Drive selected: {:?}", drive.name);
391 self.drive_selection_initial_state = self.drive_selection.clone();
392 self.assign_drive_selection();
393 self.state = ChangeDriveState::ConfirmChange;
394 vec![]
395 } else {
396 debug!("Got Enter, but no new selection. We should not do anything");
397 vec![]
398 }
399 }
400 KeyCode::Esc => {
401 debug!("Got Esc, switching to Options");
402 vec![Action::SwitchScene(Scene::Options)]
403 }
404 KeyCode::Up => {
405 if let Some(ref mut items) = self.items
406 && items.items.len() > 1
407 {
408 items.previous();
409 let drive = self.return_selection();
410 self.can_select = drive.mountpoint != self.drive_selection.mountpoint
411 && drive.status != DriveStatus::NotAvailable
412 && drive.status != DriveStatus::NotEnoughSpace;
413 }
414 vec![]
415 }
416 KeyCode::Down => {
417 if let Some(ref mut items) = self.items
418 && items.items.len() > 1
419 {
420 items.next();
421 let drive = self.return_selection();
422 self.can_select = drive.mountpoint != self.drive_selection.mountpoint
423 && drive.status != DriveStatus::NotAvailable
424 && drive.status != DriveStatus::NotEnoughSpace;
425 }
426 vec![]
427 }
428 _ => {
429 vec![]
430 }
431 }
432 }
433 ChangeDriveState::ConfirmChange => match key.code {
434 KeyCode::Enter => {
435 debug!("Got Enter, storing value and switching to Options");
436 self.drive_selection = self.return_selection();
438 match get_launchpad_nodes_data_dir_path(&self.drive_selection.mountpoint, true)
439 {
440 Ok(_path) => {
441 vec![
445 Action::StoreStorageDrive(
446 self.drive_selection.mountpoint.clone(),
447 self.drive_selection.name.clone(),
448 ),
449 Action::OptionsActions(OptionsActions::UpdateStorageDrive(
450 self.drive_selection.mountpoint.clone(),
451 self.drive_selection.name.clone(),
452 )),
453 Action::SwitchScene(Scene::Status),
454 ]
455 }
456 Err(e) => {
457 self.drive_selection = self.drive_selection_initial_state.clone();
458 self.state = ChangeDriveState::Selection;
459 error!(
460 "Error creating folder {:?}: {}",
461 self.drive_selection.mountpoint, e
462 );
463 vec![Action::SwitchScene(Scene::Options)]
464 }
465 }
466 }
467 KeyCode::Esc => {
468 debug!("Got Esc, switching to Options");
469 self.drive_selection = self.drive_selection_initial_state.clone();
470 self.state = ChangeDriveState::Selection;
471 vec![Action::SwitchScene(Scene::Options)]
472 }
473 _ => {
474 vec![]
475 }
476 },
477 };
478 Ok(send_back)
479 }
480
481 fn update(&mut self, action: Action) -> Result<Option<Action>> {
482 let send_back = match action {
483 Action::SwitchScene(scene) => match scene {
484 Scene::ChangeDrivePopUp => {
485 self.active = true;
486 self.can_select = false;
487 self.state = ChangeDriveState::Selection;
488 let _ = self.update_drive_items();
489 self.select_drive();
490 Some(Action::SwitchInputMode(InputMode::Entry))
491 }
492 _ => {
493 self.active = false;
494 None
495 }
496 },
497 Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, drive_name)) => {
499 self.drive_selection.mountpoint = mountpoint;
500 self.drive_selection.name = drive_name;
501 self.select_drive();
502 None
503 }
504 Action::StoreNodesToStart(ref nodes_to_start) => {
506 self.nodes_to_start = *nodes_to_start;
507 let _ = self.update_drive_items();
508 None
509 }
510 Action::StoreStorageDrive(mountpoint, _drive_name) => {
511 self.storage_mountpoint = mountpoint;
512 let _ = self.update_drive_items();
513 self.select_drive();
514 None
515 }
516
517 _ => None,
518 };
519 Ok(send_back)
520 }
521
522 fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
523 if !self.active {
524 return Ok(());
525 }
526
527 let layer_zero = centered_rect_fixed(52, 15, area);
528
529 let layer_one = Layout::new(
530 Direction::Vertical,
531 [
532 Constraint::Length(1),
534 Constraint::Min(1),
536 Constraint::Length(1),
538 ],
539 )
540 .split(layer_zero);
541
542 let pop_up_border: Paragraph = match self.state {
543 ChangeDriveState::Selection => self.draw_selection_state(f, layer_zero, layer_one),
544 ChangeDriveState::ConfirmChange => {
545 self.draw_confirm_change_state(f, layer_zero, layer_one)
546 }
547 };
548 f.render_widget(pop_up_border, layer_zero);
550
551 Ok(())
552 }
553}
554
555#[derive(Default, Clone)]
556struct StatefulList<T> {
557 state: ListState,
558 items: Vec<T>,
559 last_selected: Option<usize>,
560}
561
562impl<T> StatefulList<T> {
563 fn with_items(items: Vec<T>) -> Self {
564 StatefulList {
565 state: ListState::default(),
566 items,
567 last_selected: None,
568 }
569 }
570
571 fn next(&mut self) {
572 let i = match self.state.selected() {
573 Some(i) => {
574 if i >= self.items.len() - 1 {
575 0
576 } else {
577 i + 1
578 }
579 }
580 None => self.last_selected.unwrap_or(0),
581 };
582 self.state.select(Some(i));
583 }
584
585 fn previous(&mut self) {
586 let i = match self.state.selected() {
587 Some(i) => {
588 if i == 0 {
589 self.items.len() - 1
590 } else {
591 i - 1
592 }
593 }
594 None => self.last_selected.unwrap_or(0),
595 };
596 self.state.select(Some(i));
597 }
598}
599
600#[derive(Default, Debug, Copy, Clone, PartialEq)]
601enum DriveStatus {
602 Selected,
603 #[default]
604 NotSelected,
605 NotEnoughSpace,
606 NotAvailable,
607}
608
609#[derive(Default, Debug, Clone)]
610pub struct DriveItem {
611 name: String,
612 mountpoint: PathBuf,
613 size: String,
614 status: DriveStatus,
615}
616
617impl DriveItem {
618 fn to_list_item(&self, _index: usize, width: usize) -> ListItem<'_> {
619 let spaces = width - self.name.len() - self.size.len() - " ".len() - 4;
620 let line = match self.status {
621 DriveStatus::NotSelected => Line::from(vec![
622 Span::raw(" "),
623 Span::styled(self.name.clone(), Style::default().fg(VIVID_SKY_BLUE)),
624 Span::raw(" ".repeat(spaces)),
625 Span::styled(self.size.clone(), Style::default().fg(LIGHT_PERIWINKLE)),
626 ]),
627 DriveStatus::Selected => Line::from(vec![
628 Span::styled(" ►", Style::default().fg(EUCALYPTUS)),
629 Span::raw(" "),
630 Span::styled(self.name.clone(), Style::default().fg(VIVID_SKY_BLUE)),
631 Span::raw(" ".repeat(spaces)),
632 Span::styled(self.size.clone(), Style::default().fg(GHOST_WHITE)),
633 ]),
634 DriveStatus::NotEnoughSpace => Line::from(vec![
635 Span::raw(" "),
636 Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)),
637 Span::raw(" ".repeat(spaces)),
638 Span::styled(self.size.clone(), Style::default().fg(COOL_GREY)),
639 ]),
640 DriveStatus::NotAvailable => {
641 let legend = "No Access";
642 let spaces = width - self.name.len() - legend.len() - " ".len() - 4;
643 Line::from(vec![
644 Span::raw(" "),
645 Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)),
646 Span::raw(" ".repeat(spaces)),
647 Span::styled(legend, Style::default().fg(COOL_GREY)),
648 ])
649 }
650 };
651
652 ListItem::new(line)
653 }
654}