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 popup::manage_nodes::{GB, GB_PER_NODE},
28 Component,
29 },
30 config::get_launchpad_nodes_data_dir_path,
31 mode::{InputMode, Scene},
32 style::{
33 clear_area, COOL_GREY, DARK_GUNMETAL, EUCALYPTUS, GHOST_WHITE, INDIGO, LIGHT_PERIWINKLE,
34 VIVID_SKY_BLUE,
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 if let Some(i) = items.state.selected() {
94 items.items[i].status = DriveStatus::Selected;
95 self.drive_selection = items.items[i].clone();
96 }
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 if let Some(i) = items.state.selected() {
118 return items.items[i].clone();
119 }
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 if items.items.len() > 1 {
407 items.previous();
408 let drive = self.return_selection();
409 self.can_select = drive.mountpoint
410 != self.drive_selection.mountpoint
411 && drive.status != DriveStatus::NotAvailable
412 && drive.status != DriveStatus::NotEnoughSpace;
413 }
414 }
415 vec![]
416 }
417 KeyCode::Down => {
418 if let Some(ref mut items) = self.items {
419 if items.items.len() > 1 {
420 items.next();
421 let drive = self.return_selection();
422 self.can_select = drive.mountpoint
423 != self.drive_selection.mountpoint
424 && drive.status != DriveStatus::NotAvailable
425 && drive.status != DriveStatus::NotEnoughSpace;
426 }
427 }
428 vec![]
429 }
430 _ => {
431 vec![]
432 }
433 }
434 }
435 ChangeDriveState::ConfirmChange => match key.code {
436 KeyCode::Enter => {
437 debug!("Got Enter, storing value and switching to Options");
438 self.drive_selection = self.return_selection();
440 match get_launchpad_nodes_data_dir_path(&self.drive_selection.mountpoint, true)
441 {
442 Ok(_path) => {
443 vec![
447 Action::StoreStorageDrive(
448 self.drive_selection.mountpoint.clone(),
449 self.drive_selection.name.clone(),
450 ),
451 Action::OptionsActions(OptionsActions::UpdateStorageDrive(
452 self.drive_selection.mountpoint.clone(),
453 self.drive_selection.name.clone(),
454 )),
455 Action::SwitchScene(Scene::Status),
456 ]
457 }
458 Err(e) => {
459 self.drive_selection = self.drive_selection_initial_state.clone();
460 self.state = ChangeDriveState::Selection;
461 error!(
462 "Error creating folder {:?}: {}",
463 self.drive_selection.mountpoint, e
464 );
465 vec![Action::SwitchScene(Scene::Options)]
466 }
467 }
468 }
469 KeyCode::Esc => {
470 debug!("Got Esc, switching to Options");
471 self.drive_selection = self.drive_selection_initial_state.clone();
472 self.state = ChangeDriveState::Selection;
473 vec![Action::SwitchScene(Scene::Options)]
474 }
475 _ => {
476 vec![]
477 }
478 },
479 };
480 Ok(send_back)
481 }
482
483 fn update(&mut self, action: Action) -> Result<Option<Action>> {
484 let send_back = match action {
485 Action::SwitchScene(scene) => match scene {
486 Scene::ChangeDrivePopUp => {
487 self.active = true;
488 self.can_select = false;
489 self.state = ChangeDriveState::Selection;
490 let _ = self.update_drive_items();
491 self.select_drive();
492 Some(Action::SwitchInputMode(InputMode::Entry))
493 }
494 _ => {
495 self.active = false;
496 None
497 }
498 },
499 Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, drive_name)) => {
501 self.drive_selection.mountpoint = mountpoint;
502 self.drive_selection.name = drive_name;
503 self.select_drive();
504 None
505 }
506 Action::StoreNodesToStart(ref nodes_to_start) => {
508 self.nodes_to_start = *nodes_to_start;
509 let _ = self.update_drive_items();
510 None
511 }
512 Action::StoreStorageDrive(mountpoint, _drive_name) => {
513 self.storage_mountpoint = mountpoint;
514 let _ = self.update_drive_items();
515 self.select_drive();
516 None
517 }
518
519 _ => None,
520 };
521 Ok(send_back)
522 }
523
524 fn draw(&mut self, f: &mut crate::tui::Frame<'_>, area: Rect) -> Result<()> {
525 if !self.active {
526 return Ok(());
527 }
528
529 let layer_zero = centered_rect_fixed(52, 15, area);
530
531 let layer_one = Layout::new(
532 Direction::Vertical,
533 [
534 Constraint::Length(1),
536 Constraint::Min(1),
538 Constraint::Length(1),
540 ],
541 )
542 .split(layer_zero);
543
544 let pop_up_border: Paragraph = match self.state {
545 ChangeDriveState::Selection => self.draw_selection_state(f, layer_zero, layer_one),
546 ChangeDriveState::ConfirmChange => {
547 self.draw_confirm_change_state(f, layer_zero, layer_one)
548 }
549 };
550 f.render_widget(pop_up_border, layer_zero);
552
553 Ok(())
554 }
555}
556
557#[derive(Default, Clone)]
558struct StatefulList<T> {
559 state: ListState,
560 items: Vec<T>,
561 last_selected: Option<usize>,
562}
563
564impl<T> StatefulList<T> {
565 fn with_items(items: Vec<T>) -> Self {
566 StatefulList {
567 state: ListState::default(),
568 items,
569 last_selected: None,
570 }
571 }
572
573 fn next(&mut self) {
574 let i = match self.state.selected() {
575 Some(i) => {
576 if i >= self.items.len() - 1 {
577 0
578 } else {
579 i + 1
580 }
581 }
582 None => self.last_selected.unwrap_or(0),
583 };
584 self.state.select(Some(i));
585 }
586
587 fn previous(&mut self) {
588 let i = match self.state.selected() {
589 Some(i) => {
590 if i == 0 {
591 self.items.len() - 1
592 } else {
593 i - 1
594 }
595 }
596 None => self.last_selected.unwrap_or(0),
597 };
598 self.state.select(Some(i));
599 }
600}
601
602#[derive(Default, Debug, Copy, Clone, PartialEq)]
603enum DriveStatus {
604 Selected,
605 #[default]
606 NotSelected,
607 NotEnoughSpace,
608 NotAvailable,
609}
610
611#[derive(Default, Debug, Clone)]
612pub struct DriveItem {
613 name: String,
614 mountpoint: PathBuf,
615 size: String,
616 status: DriveStatus,
617}
618
619impl DriveItem {
620 fn to_list_item(&self, _index: usize, width: usize) -> ListItem {
621 let spaces = width - self.name.len() - self.size.len() - " ".len() - 4;
622 let line = match self.status {
623 DriveStatus::NotSelected => Line::from(vec![
624 Span::raw(" "),
625 Span::styled(self.name.clone(), Style::default().fg(VIVID_SKY_BLUE)),
626 Span::raw(" ".repeat(spaces)),
627 Span::styled(self.size.clone(), Style::default().fg(LIGHT_PERIWINKLE)),
628 ]),
629 DriveStatus::Selected => Line::from(vec![
630 Span::styled(" ►", Style::default().fg(EUCALYPTUS)),
631 Span::raw(" "),
632 Span::styled(self.name.clone(), Style::default().fg(VIVID_SKY_BLUE)),
633 Span::raw(" ".repeat(spaces)),
634 Span::styled(self.size.clone(), Style::default().fg(GHOST_WHITE)),
635 ]),
636 DriveStatus::NotEnoughSpace => Line::from(vec![
637 Span::raw(" "),
638 Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)),
639 Span::raw(" ".repeat(spaces)),
640 Span::styled(self.size.clone(), Style::default().fg(COOL_GREY)),
641 ]),
642 DriveStatus::NotAvailable => {
643 let legend = "No Access";
644 let spaces = width - self.name.len() - legend.len() - " ".len() - 4;
645 Line::from(vec![
646 Span::raw(" "),
647 Span::styled(self.name.clone(), Style::default().fg(COOL_GREY)),
648 Span::raw(" ".repeat(spaces)),
649 Span::styled(legend, Style::default().fg(COOL_GREY)),
650 ])
651 }
652 };
653
654 ListItem::new(line)
655 }
656}