feat: delete container, closes #27
Enable a user to delete a container. A dialog will pop up to ask the user to confirm the deletion. A user can then click on either button, or press N/Y to make a selection
This commit is contained in:
+116
-16
@@ -21,7 +21,7 @@ use crate::{
|
||||
app_error::AppError,
|
||||
};
|
||||
|
||||
use super::gui_state::{BoxLocation, Region};
|
||||
use super::gui_state::{BoxLocation, DeleteButton, Region};
|
||||
use super::{GuiState, SelectablePanel};
|
||||
|
||||
const NAME_TEXT: &str = r#"
|
||||
@@ -43,6 +43,14 @@ const MARGIN: &str = " ";
|
||||
const ARROW: &str = "▶ ";
|
||||
const CIRCLE: &str = "⚪ ";
|
||||
|
||||
/// From a given &str, return the maximum number of chars on a single line
|
||||
fn max_line_width(text: &str) -> usize {
|
||||
text.lines()
|
||||
.map(|i| i.chars().count())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Generate block, add a border if is the selected panel,
|
||||
/// add custom title based on state of each panel
|
||||
fn generate_block<'a>(
|
||||
@@ -53,7 +61,7 @@ fn generate_block<'a>(
|
||||
) -> Block<'a> {
|
||||
gui_state
|
||||
.lock()
|
||||
.update_heading_map(Region::Panel(panel), area);
|
||||
.update_region_map(Region::Panel(panel), area);
|
||||
let current_selected_panel = gui_state.lock().selected_panel;
|
||||
let mut title = match panel {
|
||||
SelectablePanel::Containers => {
|
||||
@@ -459,7 +467,7 @@ pub fn heading_bar<B: Backend>(
|
||||
let rect = headers_section[index];
|
||||
gui_state
|
||||
.lock()
|
||||
.update_heading_map(Region::Header(header), rect);
|
||||
.update_region_map(Region::Header(header), rect);
|
||||
f.render_widget(paragraph, rect);
|
||||
}
|
||||
}
|
||||
@@ -479,14 +487,6 @@ pub fn heading_bar<B: Backend>(
|
||||
f.render_widget(help_paragraph, split_bar[help_index]);
|
||||
}
|
||||
|
||||
/// From a given &str, return the maximum number of chars on a single line
|
||||
fn max_line_width(text: &str) -> usize {
|
||||
text.lines()
|
||||
.map(|i| i.chars().count())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Help popup box needs these three pieces of information
|
||||
struct HelpInfo {
|
||||
spans: Vec<Spans<'static>>,
|
||||
@@ -593,11 +593,7 @@ impl HelpInfo {
|
||||
button_item("h"),
|
||||
button_desc("to toggle this help information"),
|
||||
]),
|
||||
Spans::from(vec![
|
||||
space(),
|
||||
button_item("0"),
|
||||
button_desc("to stop sort"),
|
||||
]),
|
||||
Spans::from(vec![space(), button_item("0"), button_desc("to stop sort")]),
|
||||
Spans::from(vec![
|
||||
space(),
|
||||
button_item("1 - 9"),
|
||||
@@ -728,6 +724,110 @@ pub fn help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
|
||||
/// Draw the delete confirm box in the centre of the screen
|
||||
/// take in container id and container name here?
|
||||
pub fn delete_confirm<B: Backend>(
|
||||
f: &mut Frame<'_, B>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
name: &str,
|
||||
) {
|
||||
let block = Block::default()
|
||||
.title(" Confirm Delete ")
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().bg(Color::White).fg(Color::Black))
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let confirm = Spans::from(vec![
|
||||
Span::from("Are you sure you want to delete container: "),
|
||||
Span::styled(
|
||||
name,
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
|
||||
),
|
||||
]);
|
||||
|
||||
let yes_text = " (Y)es ";
|
||||
let no_text = " (N)o ";
|
||||
|
||||
// Find the maximum line width & height, and add some padding
|
||||
let max_line_width = u16::try_from(confirm.width()).unwrap_or(64) + 12;
|
||||
let lines = 8;
|
||||
|
||||
let confirm_para = Paragraph::new(confirm).alignment(Alignment::Center);
|
||||
|
||||
let button_block = || {
|
||||
Block::default()
|
||||
.border_type(BorderType::Rounded)
|
||||
.borders(Borders::ALL)
|
||||
};
|
||||
|
||||
let yes_para = Paragraph::new(yes_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(button_block());
|
||||
// Need to add some padding for the borders
|
||||
let yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9);
|
||||
|
||||
let no_para = Paragraph::new(no_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(button_block());
|
||||
// Need to add some padding for the borders
|
||||
let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8);
|
||||
|
||||
let area = popup(
|
||||
lines,
|
||||
max_line_width.into(),
|
||||
f.size(),
|
||||
BoxLocation::MiddleCentre,
|
||||
);
|
||||
|
||||
let split_popup = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(2),
|
||||
Constraint::Max(1),
|
||||
Constraint::Max(1),
|
||||
Constraint::Max(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
|
||||
let button_spacing = (max_line_width - no_chars - yes_chars) / 3;
|
||||
let split_buttons = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(button_spacing),
|
||||
Constraint::Max(no_chars),
|
||||
Constraint::Min(button_spacing),
|
||||
Constraint::Max(yes_chars),
|
||||
Constraint::Min(button_spacing),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(split_popup[3]);
|
||||
|
||||
let no_area = split_buttons[1];
|
||||
let yes_area = split_buttons[3];
|
||||
|
||||
// Insert button areas into region map, so can interact with them on click
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Delete(DeleteButton::No), no_area);
|
||||
|
||||
gui_state
|
||||
.lock()
|
||||
.update_region_map(Region::Delete(DeleteButton::Yes), yes_area);
|
||||
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(block, area);
|
||||
f.render_widget(confirm_para, split_popup[1]);
|
||||
f.render_widget(no_para, no_area);
|
||||
f.render_widget(yes_para, yes_area);
|
||||
}
|
||||
|
||||
/// Draw an error popup over whole screen
|
||||
pub fn error<B: Backend>(f: &mut Frame<'_, B>, error: AppError, seconds: Option<u8>) {
|
||||
let block = Block::default()
|
||||
|
||||
+47
-2
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::app_data::Header;
|
||||
use crate::app_data::{ContainerId, Header};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum SelectablePanel {
|
||||
@@ -43,6 +43,13 @@ impl SelectablePanel {
|
||||
pub enum Region {
|
||||
Panel(SelectablePanel),
|
||||
Header(Header),
|
||||
Delete(DeleteButton),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub enum DeleteButton {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -191,11 +198,13 @@ impl fmt::Display for Loading {
|
||||
|
||||
/// The application gui state can be in multiple of these four states at the same time
|
||||
/// Various functions (e.g input handler), operate differently depending upon current Status
|
||||
// Copy
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
Init,
|
||||
Help,
|
||||
DockerConnect,
|
||||
DeleteConfirm,
|
||||
Error,
|
||||
}
|
||||
|
||||
@@ -206,7 +215,9 @@ pub struct GuiState {
|
||||
is_loading: HashSet<Uuid>,
|
||||
loading_icon: Loading,
|
||||
panel_map: HashMap<SelectablePanel, Rect>,
|
||||
delete_map: HashMap<DeleteButton, Rect>,
|
||||
status: HashSet<Status>,
|
||||
delete_container: Option<ContainerId>,
|
||||
pub info_box_text: Option<String>,
|
||||
pub selected_panel: SelectablePanel,
|
||||
}
|
||||
@@ -229,6 +240,16 @@ impl GuiState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button
|
||||
pub fn button_intersect(&mut self, rect: Rect) -> Option<DeleteButton> {
|
||||
self.delete_map
|
||||
.iter()
|
||||
.filter(|i| i.1.intersects(rect))
|
||||
.collect::<Vec<_>>()
|
||||
.get(0)
|
||||
.map(|data| *data.0)
|
||||
}
|
||||
|
||||
/// Check if a given Rect (a clicked area of 1x1), interacts with any known panels
|
||||
pub fn header_intersect(&mut self, rect: Rect) -> Option<Header> {
|
||||
self.heading_map
|
||||
@@ -240,7 +261,7 @@ impl GuiState {
|
||||
}
|
||||
|
||||
/// Insert, or updates header area panel into heading_map
|
||||
pub fn update_heading_map(&mut self, region: Region, area: Rect) {
|
||||
pub fn update_region_map(&mut self, region: Region, area: Rect) {
|
||||
match region {
|
||||
Region::Header(header) => self
|
||||
.heading_map
|
||||
@@ -252,9 +273,30 @@ impl GuiState {
|
||||
.entry(panel)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area),
|
||||
Region::Delete(button) => self
|
||||
.delete_map
|
||||
.entry(button)
|
||||
.and_modify(|w| *w = area)
|
||||
.or_insert(area),
|
||||
};
|
||||
}
|
||||
|
||||
/// Check if an ContainerId is set in the delete_container field
|
||||
pub fn get_delete_container(&self) -> Option<ContainerId> {
|
||||
self.delete_container.clone()
|
||||
}
|
||||
|
||||
/// Set either a ContainerId, or None, to the delete_container field
|
||||
/// If Some, will also insert the DeleteConfirm status into self.status
|
||||
pub fn set_delete_container(&mut self, id: Option<ContainerId>) {
|
||||
if id.is_some() {
|
||||
self.status.insert(Status::DeleteConfirm);
|
||||
} else {
|
||||
self.status.remove(&Status::DeleteConfirm);
|
||||
}
|
||||
self.delete_container = id;
|
||||
}
|
||||
|
||||
/// Check if the current gui_status contains any of the given status'
|
||||
/// Don't really like this methodology for gui state, needs a re-think
|
||||
pub fn status_contains(&self, status: &[Status]) -> bool {
|
||||
@@ -264,6 +306,9 @@ impl GuiState {
|
||||
/// Remove a gui_status into the current gui_status HashSet
|
||||
pub fn status_del(&mut self, status: Status) {
|
||||
self.status.remove(&status);
|
||||
if status == Status::DeleteConfirm {
|
||||
self.status.remove(&Status::DeleteConfirm);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a gui_status into the current gui_status HashSet
|
||||
|
||||
+20
-3
@@ -24,7 +24,7 @@ mod draw_blocks;
|
||||
mod gui_state;
|
||||
|
||||
pub use self::color_match::*;
|
||||
pub use self::gui_state::{GuiState, SelectablePanel, Status};
|
||||
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
||||
use crate::{
|
||||
app_data::AppData, app_error::AppError, docker_data::DockerMessage,
|
||||
input_handler::InputMessages,
|
||||
@@ -198,20 +198,23 @@ impl Ui {
|
||||
}
|
||||
|
||||
/// Draw the main ui to a frame of the terminal
|
||||
/// TODO add a single line area for debug message - if not in release mode, maybe with #[cfg(debug_assertions)] ?
|
||||
fn draw_frame<B: Backend>(
|
||||
f: &mut Frame<'_, B>,
|
||||
app_data: &Arc<Mutex<AppData>>,
|
||||
gui_state: &Arc<Mutex<GuiState>>,
|
||||
) {
|
||||
// set max height for container section, needs +4 to deal with docker commands list and borders
|
||||
// set max height for container section, needs +5 to deal with docker commands list and borders
|
||||
let height = app_data.lock().get_container_len();
|
||||
let height = if height < 12 { height + 4 } else { 12 };
|
||||
let height = if height < 12 { height + 5 } else { 12 };
|
||||
|
||||
let column_widths = app_data.lock().get_width();
|
||||
let has_containers = app_data.lock().get_container_len() > 0;
|
||||
let has_error = app_data.lock().get_error();
|
||||
let sorted_by = app_data.lock().get_sorted();
|
||||
|
||||
let delete_confirm = gui_state.lock().get_delete_container();
|
||||
|
||||
let show_help = gui_state.lock().status_contains(&[Status::Help]);
|
||||
let info_text = gui_state.lock().info_box_text.clone();
|
||||
let loading_icon = gui_state.lock().get_loading();
|
||||
@@ -274,6 +277,20 @@ fn draw_frame<B: Backend>(
|
||||
gui_state,
|
||||
);
|
||||
|
||||
if let Some(id) = delete_confirm {
|
||||
let name = app_data.lock().get_container_name_by_id(&id);
|
||||
name.map_or_else(
|
||||
|| {
|
||||
// If a container is deleted outside of oxker but whilst the Delete Confirm dialog is open, it can get caught in kind of a dead lock situation
|
||||
// so if in that unique situation, just clear the delete_container id
|
||||
gui_state.lock().set_delete_container(None);
|
||||
},
|
||||
|name| {
|
||||
draw_blocks::delete_confirm(f, gui_state, &name);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// only draw charts if there are containers
|
||||
if has_containers {
|
||||
draw_blocks::chart(f, lower_main[1], app_data);
|
||||
|
||||
Reference in New Issue
Block a user