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:
@@ -207,6 +207,7 @@ pub enum DockerControls {
|
|||||||
Start,
|
Start,
|
||||||
Stop,
|
Stop,
|
||||||
Unpause,
|
Unpause,
|
||||||
|
Delete,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DockerControls {
|
impl DockerControls {
|
||||||
@@ -216,6 +217,7 @@ impl DockerControls {
|
|||||||
Self::Restart => Color::Magenta,
|
Self::Restart => Color::Magenta,
|
||||||
Self::Start => Color::Green,
|
Self::Start => Color::Green,
|
||||||
Self::Stop => Color::Red,
|
Self::Stop => Color::Red,
|
||||||
|
Self::Delete => Color::Gray,
|
||||||
Self::Unpause => Color::Blue,
|
Self::Unpause => Color::Blue,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,11 +225,11 @@ impl DockerControls {
|
|||||||
/// Docker commands available depending on the containers state
|
/// Docker commands available depending on the containers state
|
||||||
pub fn gen_vec(state: State) -> Vec<Self> {
|
pub fn gen_vec(state: State) -> Vec<Self> {
|
||||||
match state {
|
match state {
|
||||||
State::Dead | State::Exited => vec![Self::Start, Self::Restart],
|
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
|
||||||
State::Paused => vec![Self::Unpause, Self::Stop],
|
State::Paused => vec![Self::Unpause, Self::Stop, Self::Delete],
|
||||||
State::Restarting => vec![Self::Stop],
|
State::Restarting => vec![Self::Stop, Self::Delete],
|
||||||
State::Running => vec![Self::Pause, Self::Restart, Self::Stop],
|
State::Running => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete],
|
||||||
_ => vec![],
|
_ => vec![Self::Delete],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,6 +238,7 @@ impl fmt::Display for DockerControls {
|
|||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let disp = match self {
|
let disp = match self {
|
||||||
Self::Pause => "pause",
|
Self::Pause => "pause",
|
||||||
|
Self::Delete => "delete",
|
||||||
Self::Restart => "restart",
|
Self::Restart => "restart",
|
||||||
Self::Start => "start",
|
Self::Start => "start",
|
||||||
Self::Stop => "stop",
|
Self::Stop => "stop",
|
||||||
|
|||||||
@@ -456,6 +456,15 @@ impl AppData {
|
|||||||
self.containers.items.iter_mut().find(|i| &i.id == id)
|
self.containers.items.iter_mut().find(|i| &i.id == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// return a mutable container by given id
|
||||||
|
pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<String> {
|
||||||
|
self.containers
|
||||||
|
.items
|
||||||
|
.iter_mut()
|
||||||
|
.find(|i| &i.id == id)
|
||||||
|
.map(|i| i.name.clone())
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the id of the currently selected container.
|
/// Find the id of the currently selected container.
|
||||||
/// If any containers on system, will always return a ContainerId
|
/// If any containers on system, will always return a ContainerId
|
||||||
/// Only returns None when no containers found.
|
/// Only returns None when no containers found.
|
||||||
@@ -532,6 +541,7 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trim a &String and return String
|
// Trim a &String and return String
|
||||||
let trim_owned = |x: &String| x.trim().to_owned();
|
let trim_owned = |x: &String| x.trim().to_owned();
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ use crate::app_data::ContainerId;
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DockerMessage {
|
pub enum DockerMessage {
|
||||||
Update,
|
Delete(ContainerId),
|
||||||
Start(ContainerId),
|
ConfirmDelete(ContainerId),
|
||||||
Restart(ContainerId),
|
|
||||||
Pause(ContainerId),
|
Pause(ContainerId),
|
||||||
Unpause(ContainerId),
|
|
||||||
Stop(ContainerId),
|
|
||||||
Quit,
|
Quit,
|
||||||
|
Restart(ContainerId),
|
||||||
|
Start(ContainerId),
|
||||||
|
Stop(ContainerId),
|
||||||
|
Unpause(ContainerId),
|
||||||
|
Update,
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-2
@@ -1,5 +1,8 @@
|
|||||||
use bollard::{
|
use bollard::{
|
||||||
container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions},
|
container::{
|
||||||
|
ListContainersOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions, Stats,
|
||||||
|
StatsOptions,
|
||||||
|
},
|
||||||
service::ContainerSummary,
|
service::ContainerSummary,
|
||||||
Docker,
|
Docker,
|
||||||
};
|
};
|
||||||
@@ -335,13 +338,14 @@ impl DockerData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle incoming messages, container controls & all container information update
|
/// Handle incoming messages, container controls & all container information update
|
||||||
/// Spawn dowcker commands off into own thread
|
/// Spawn Docker commands off into own thread
|
||||||
async fn message_handler(&mut self) {
|
async fn message_handler(&mut self) {
|
||||||
while let Some(message) = self.receiver.recv().await {
|
while let Some(message) = self.receiver.recv().await {
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let gui_state = Arc::clone(&self.gui_state);
|
let gui_state = Arc::clone(&self.gui_state);
|
||||||
let app_data = Arc::clone(&self.app_data);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
|
// TODO need to refactor these
|
||||||
match message {
|
match message {
|
||||||
DockerMessage::Pause(id) => {
|
DockerMessage::Pause(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -397,6 +401,31 @@ impl DockerData {
|
|||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
|
DockerMessage::Delete(id) => {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let loading_spin = Self::loading_spin(uuid, &gui_state).await;
|
||||||
|
if docker
|
||||||
|
.remove_container(
|
||||||
|
id.get(),
|
||||||
|
Some(RemoveContainerOptions {
|
||||||
|
v: false,
|
||||||
|
force: true,
|
||||||
|
link: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
||||||
|
}
|
||||||
|
Self::stop_loading_spin(&gui_state, &loading_spin, uuid);
|
||||||
|
});
|
||||||
|
self.update_everything().await;
|
||||||
|
self.gui_state.lock().set_delete_container(None);
|
||||||
|
}
|
||||||
|
DockerMessage::ConfirmDelete(id) => {
|
||||||
|
self.gui_state.lock().set_delete_container(Some(id))
|
||||||
|
}
|
||||||
DockerMessage::Update => self.update_everything().await,
|
DockerMessage::Update => self.update_everything().await,
|
||||||
DockerMessage::Quit => {
|
DockerMessage::Quit => {
|
||||||
self.spawns
|
self.spawns
|
||||||
|
|||||||
+68
-19
@@ -19,7 +19,7 @@ use crate::{
|
|||||||
app_data::{AppData, DockerControls, Header},
|
app_data::{AppData, DockerControls, Header},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
docker_data::DockerMessage,
|
docker_data::DockerMessage,
|
||||||
ui::{GuiState, SelectablePanel, Status, Ui},
|
ui::{DeleteButton, GuiState, SelectablePanel, Status, Ui},
|
||||||
};
|
};
|
||||||
pub use message::InputMessages;
|
pub use message::InputMessages;
|
||||||
|
|
||||||
@@ -62,13 +62,21 @@ impl InputHandler {
|
|||||||
match message {
|
match message {
|
||||||
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
|
InputMessages::ButtonPress(key) => self.button_press(key.0, key.1).await,
|
||||||
InputMessages::MouseEvent(mouse_event) => {
|
InputMessages::MouseEvent(mouse_event) => {
|
||||||
let error_or_help = self
|
let error_or_help = self.gui_state.lock().status_contains(&[
|
||||||
.gui_state
|
Status::Error,
|
||||||
.lock()
|
Status::Help,
|
||||||
.status_contains(&[Status::Error, Status::Help]);
|
Status::DeleteConfirm,
|
||||||
|
]);
|
||||||
if !error_or_help {
|
if !error_or_help {
|
||||||
self.mouse_press(mouse_event);
|
self.mouse_press(mouse_event);
|
||||||
}
|
}
|
||||||
|
let delete_confirm = self
|
||||||
|
.gui_state
|
||||||
|
.lock()
|
||||||
|
.status_contains(&[Status::DeleteConfirm]);
|
||||||
|
if delete_confirm {
|
||||||
|
self.button_intersect(mouse_event).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !self.is_running.load(Ordering::SeqCst) {
|
if !self.is_running.load(Ordering::SeqCst) {
|
||||||
@@ -133,41 +141,59 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This is executed from the Delete Confirm dialog, and will send an internal message to actually remove the given container
|
||||||
|
async fn confirm_delete(&self) {
|
||||||
|
let id = self.gui_state.lock().get_delete_container();
|
||||||
|
if let Some(id) = id {
|
||||||
|
self.docker_sender
|
||||||
|
.send(DockerMessage::Delete(id))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is executed from the Delete Confirm dialog, and will clear the delete_container information (removes id and closes panel)
|
||||||
|
fn clear_delete(&self) {
|
||||||
|
self.gui_state.lock().set_delete_container(None);
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle any keyboard button events
|
/// Handle any keyboard button events
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
|
async fn button_press(&mut self, key_code: KeyCode, key_modififer: KeyModifiers) {
|
||||||
// TODO - refactor this to a single call, maybe return Error, Help or Normal
|
// TODO - refactor this to a single call, maybe return Error, Help or Normal
|
||||||
let contains_error = self.gui_state.lock().status_contains(&[Status::Error]);
|
let contains_error = self.gui_state.lock().status_contains(&[Status::Error]);
|
||||||
let contains_help = self.gui_state.lock().status_contains(&[Status::Help]);
|
let contains_help = self.gui_state.lock().status_contains(&[Status::Help]);
|
||||||
|
let contains_delete = self
|
||||||
|
.gui_state
|
||||||
|
.lock()
|
||||||
|
.status_contains(&[Status::DeleteConfirm]);
|
||||||
|
|
||||||
// Quit on Ctrl + c/C
|
// Always just quit on Ctrl + c/C or q/Q
|
||||||
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
||||||
if key_modififer == KeyModifiers::CONTROL && is_c() {
|
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
||||||
|
if key_modififer == KeyModifiers::CONTROL && is_c() || is_q() {
|
||||||
self.quit().await;
|
self.quit().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if contains_error {
|
if contains_error {
|
||||||
match key_code {
|
if let KeyCode::Char('c' | 'C') = key_code {
|
||||||
KeyCode::Char('q' | 'Q') => self.quit().await,
|
|
||||||
KeyCode::Char('c' | 'C') => {
|
|
||||||
self.app_data.lock().remove_error();
|
self.app_data.lock().remove_error();
|
||||||
self.gui_state.lock().status_del(Status::Error);
|
self.gui_state.lock().status_del(Status::Error);
|
||||||
}
|
}
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
} else if contains_help {
|
} else if contains_help {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('q' | 'Q') => self.quit().await,
|
|
||||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help),
|
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_del(Status::Help),
|
||||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
} else {
|
} else if contains_delete {
|
||||||
// let abc = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::Ctrl);
|
match key_code {
|
||||||
|
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
||||||
|
KeyCode::Char('n' | 'N') => self.clear_delete(),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
match key_code {
|
match key_code {
|
||||||
// KeyCode::Ctrl('c') => {
|
|
||||||
// self.quit().await;
|
|
||||||
// }
|
|
||||||
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
||||||
KeyCode::Char('1') => self.sort(Header::State),
|
KeyCode::Char('1') => self.sort(Header::State),
|
||||||
KeyCode::Char('2') => self.sort(Header::Status),
|
KeyCode::Char('2') => self.sort(Header::Status),
|
||||||
@@ -178,7 +204,6 @@ impl InputHandler {
|
|||||||
KeyCode::Char('7') => self.sort(Header::Image),
|
KeyCode::Char('7') => self.sort(Header::Image),
|
||||||
KeyCode::Char('8') => self.sort(Header::Rx),
|
KeyCode::Char('8') => self.sort(Header::Rx),
|
||||||
KeyCode::Char('9') => self.sort(Header::Tx),
|
KeyCode::Char('9') => self.sort(Header::Tx),
|
||||||
KeyCode::Char('q' | 'Q') => self.quit().await,
|
|
||||||
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
KeyCode::Char('h' | 'H') => self.gui_state.lock().status_push(Status::Help),
|
||||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||||
KeyCode::Tab => {
|
KeyCode::Tab => {
|
||||||
@@ -251,6 +276,11 @@ impl InputHandler {
|
|||||||
};
|
};
|
||||||
if let Some(id) = option_id {
|
if let Some(id) = option_id {
|
||||||
match command {
|
match command {
|
||||||
|
DockerControls::Delete => self
|
||||||
|
.docker_sender
|
||||||
|
.send(DockerMessage::ConfirmDelete(id))
|
||||||
|
.await
|
||||||
|
.ok(),
|
||||||
DockerControls::Pause => {
|
DockerControls::Pause => {
|
||||||
self.docker_sender.send(DockerMessage::Pause(id)).await.ok()
|
self.docker_sender.send(DockerMessage::Pause(id)).await.ok()
|
||||||
}
|
}
|
||||||
@@ -280,6 +310,25 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window
|
||||||
|
async fn button_intersect(&mut self, mouse_event: MouseEvent) {
|
||||||
|
if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
|
||||||
|
let intersect = self.gui_state.lock().button_intersect(Rect::new(
|
||||||
|
mouse_event.column,
|
||||||
|
mouse_event.row,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(button) = intersect {
|
||||||
|
match button {
|
||||||
|
DeleteButton::Yes => self.confirm_delete().await,
|
||||||
|
DeleteButton::No => self.clear_delete(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle mouse button events
|
/// Handle mouse button events
|
||||||
fn mouse_press(&mut self, mouse_event: MouseEvent) {
|
fn mouse_press(&mut self, mouse_event: MouseEvent) {
|
||||||
match mouse_event.kind {
|
match mouse_event.kind {
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ use ui::{GuiState, Status, Ui};
|
|||||||
|
|
||||||
use crate::docker_data::DockerMessage;
|
use crate::docker_data::DockerMessage;
|
||||||
|
|
||||||
// this is the entry point when running as a Docker Container, and is used, in conjunction with the `CONTAINER_ENV` ENV, to check if we are running as a Docker Container
|
/// This is the entry point when running as a Docker Container, and is used, in conjunction with the `CONTAINER_ENV` ENV, to check if we are running as a Docker Container
|
||||||
const ENTRY_POINT: &str = "/app/oxker";
|
const ENTRY_POINT: &str = "/app/oxker";
|
||||||
const ENV_KEY: &str = "OXKER_RUNTIME";
|
const ENV_KEY: &str = "OXKER_RUNTIME";
|
||||||
const ENV_VALUE: &str = "container";
|
const ENV_VALUE: &str = "container";
|
||||||
|
|||||||
+116
-16
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::gui_state::{BoxLocation, Region};
|
use super::gui_state::{BoxLocation, DeleteButton, Region};
|
||||||
use super::{GuiState, SelectablePanel};
|
use super::{GuiState, SelectablePanel};
|
||||||
|
|
||||||
const NAME_TEXT: &str = r#"
|
const NAME_TEXT: &str = r#"
|
||||||
@@ -43,6 +43,14 @@ const MARGIN: &str = " ";
|
|||||||
const ARROW: &str = "▶ ";
|
const ARROW: &str = "▶ ";
|
||||||
const CIRCLE: &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,
|
/// Generate block, add a border if is the selected panel,
|
||||||
/// add custom title based on state of each panel
|
/// add custom title based on state of each panel
|
||||||
fn generate_block<'a>(
|
fn generate_block<'a>(
|
||||||
@@ -53,7 +61,7 @@ fn generate_block<'a>(
|
|||||||
) -> Block<'a> {
|
) -> Block<'a> {
|
||||||
gui_state
|
gui_state
|
||||||
.lock()
|
.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 current_selected_panel = gui_state.lock().selected_panel;
|
||||||
let mut title = match panel {
|
let mut title = match panel {
|
||||||
SelectablePanel::Containers => {
|
SelectablePanel::Containers => {
|
||||||
@@ -459,7 +467,7 @@ pub fn heading_bar<B: Backend>(
|
|||||||
let rect = headers_section[index];
|
let rect = headers_section[index];
|
||||||
gui_state
|
gui_state
|
||||||
.lock()
|
.lock()
|
||||||
.update_heading_map(Region::Header(header), rect);
|
.update_region_map(Region::Header(header), rect);
|
||||||
f.render_widget(paragraph, 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]);
|
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
|
/// Help popup box needs these three pieces of information
|
||||||
struct HelpInfo {
|
struct HelpInfo {
|
||||||
spans: Vec<Spans<'static>>,
|
spans: Vec<Spans<'static>>,
|
||||||
@@ -593,11 +593,7 @@ impl HelpInfo {
|
|||||||
button_item("h"),
|
button_item("h"),
|
||||||
button_desc("to toggle this help information"),
|
button_desc("to toggle this help information"),
|
||||||
]),
|
]),
|
||||||
Spans::from(vec![
|
Spans::from(vec![space(), button_item("0"), button_desc("to stop sort")]),
|
||||||
space(),
|
|
||||||
button_item("0"),
|
|
||||||
button_desc("to stop sort"),
|
|
||||||
]),
|
|
||||||
Spans::from(vec![
|
Spans::from(vec![
|
||||||
space(),
|
space(),
|
||||||
button_item("1 - 9"),
|
button_item("1 - 9"),
|
||||||
@@ -728,6 +724,110 @@ pub fn help_box<B: Backend>(f: &mut Frame<'_, B>) {
|
|||||||
f.render_widget(block, area);
|
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
|
/// Draw an error popup over whole screen
|
||||||
pub fn error<B: Backend>(f: &mut Frame<'_, B>, error: AppError, seconds: Option<u8>) {
|
pub fn error<B: Backend>(f: &mut Frame<'_, B>, error: AppError, seconds: Option<u8>) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
|
|||||||
+47
-2
@@ -5,7 +5,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::app_data::Header;
|
use crate::app_data::{ContainerId, Header};
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
|
#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq)]
|
||||||
pub enum SelectablePanel {
|
pub enum SelectablePanel {
|
||||||
@@ -43,6 +43,13 @@ impl SelectablePanel {
|
|||||||
pub enum Region {
|
pub enum Region {
|
||||||
Panel(SelectablePanel),
|
Panel(SelectablePanel),
|
||||||
Header(Header),
|
Header(Header),
|
||||||
|
Delete(DeleteButton),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
|
||||||
|
pub enum DeleteButton {
|
||||||
|
Yes,
|
||||||
|
No,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[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
|
/// 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
|
/// Various functions (e.g input handler), operate differently depending upon current Status
|
||||||
|
// Copy
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
pub enum Status {
|
pub enum Status {
|
||||||
Init,
|
Init,
|
||||||
Help,
|
Help,
|
||||||
DockerConnect,
|
DockerConnect,
|
||||||
|
DeleteConfirm,
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +215,9 @@ pub struct GuiState {
|
|||||||
is_loading: HashSet<Uuid>,
|
is_loading: HashSet<Uuid>,
|
||||||
loading_icon: Loading,
|
loading_icon: Loading,
|
||||||
panel_map: HashMap<SelectablePanel, Rect>,
|
panel_map: HashMap<SelectablePanel, Rect>,
|
||||||
|
delete_map: HashMap<DeleteButton, Rect>,
|
||||||
status: HashSet<Status>,
|
status: HashSet<Status>,
|
||||||
|
delete_container: Option<ContainerId>,
|
||||||
pub info_box_text: Option<String>,
|
pub info_box_text: Option<String>,
|
||||||
pub selected_panel: SelectablePanel,
|
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
|
/// 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> {
|
pub fn header_intersect(&mut self, rect: Rect) -> Option<Header> {
|
||||||
self.heading_map
|
self.heading_map
|
||||||
@@ -240,7 +261,7 @@ impl GuiState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Insert, or updates header area panel into heading_map
|
/// 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 {
|
match region {
|
||||||
Region::Header(header) => self
|
Region::Header(header) => self
|
||||||
.heading_map
|
.heading_map
|
||||||
@@ -252,9 +273,30 @@ impl GuiState {
|
|||||||
.entry(panel)
|
.entry(panel)
|
||||||
.and_modify(|w| *w = area)
|
.and_modify(|w| *w = area)
|
||||||
.or_insert(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'
|
/// 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
|
/// Don't really like this methodology for gui state, needs a re-think
|
||||||
pub fn status_contains(&self, status: &[Status]) -> bool {
|
pub fn status_contains(&self, status: &[Status]) -> bool {
|
||||||
@@ -264,6 +306,9 @@ impl GuiState {
|
|||||||
/// Remove a gui_status into the current gui_status HashSet
|
/// Remove a gui_status into the current gui_status HashSet
|
||||||
pub fn status_del(&mut self, status: Status) {
|
pub fn status_del(&mut self, status: Status) {
|
||||||
self.status.remove(&status);
|
self.status.remove(&status);
|
||||||
|
if status == Status::DeleteConfirm {
|
||||||
|
self.status.remove(&Status::DeleteConfirm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a gui_status into the current gui_status HashSet
|
/// Insert a gui_status into the current gui_status HashSet
|
||||||
|
|||||||
+20
-3
@@ -24,7 +24,7 @@ mod draw_blocks;
|
|||||||
mod gui_state;
|
mod gui_state;
|
||||||
|
|
||||||
pub use self::color_match::*;
|
pub use self::color_match::*;
|
||||||
pub use self::gui_state::{GuiState, SelectablePanel, Status};
|
pub use self::gui_state::{DeleteButton, GuiState, SelectablePanel, Status};
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::AppData, app_error::AppError, docker_data::DockerMessage,
|
app_data::AppData, app_error::AppError, docker_data::DockerMessage,
|
||||||
input_handler::InputMessages,
|
input_handler::InputMessages,
|
||||||
@@ -198,20 +198,23 @@ impl Ui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the main ui to a frame of the terminal
|
/// 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>(
|
fn draw_frame<B: Backend>(
|
||||||
f: &mut Frame<'_, B>,
|
f: &mut Frame<'_, B>,
|
||||||
app_data: &Arc<Mutex<AppData>>,
|
app_data: &Arc<Mutex<AppData>>,
|
||||||
gui_state: &Arc<Mutex<GuiState>>,
|
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 = 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 column_widths = app_data.lock().get_width();
|
||||||
let has_containers = app_data.lock().get_container_len() > 0;
|
let has_containers = app_data.lock().get_container_len() > 0;
|
||||||
let has_error = app_data.lock().get_error();
|
let has_error = app_data.lock().get_error();
|
||||||
let sorted_by = app_data.lock().get_sorted();
|
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 show_help = gui_state.lock().status_contains(&[Status::Help]);
|
||||||
let info_text = gui_state.lock().info_box_text.clone();
|
let info_text = gui_state.lock().info_box_text.clone();
|
||||||
let loading_icon = gui_state.lock().get_loading();
|
let loading_icon = gui_state.lock().get_loading();
|
||||||
@@ -274,6 +277,20 @@ fn draw_frame<B: Backend>(
|
|||||||
gui_state,
|
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
|
// only draw charts if there are containers
|
||||||
if has_containers {
|
if has_containers {
|
||||||
draw_blocks::chart(f, lower_main[1], app_data);
|
draw_blocks::chart(f, lower_main[1], app_data);
|
||||||
|
|||||||
Reference in New Issue
Block a user