feat: unhealthy status, closes #43
Highlight an unhealthy container in Orange, and display "! running" as the state, refactor: Move dev Docker files to docker directory
This commit is contained in:
+133
-36
@@ -10,6 +10,8 @@ use ratatui::{
|
||||
widgets::{ListItem, ListState},
|
||||
};
|
||||
|
||||
use crate::ui::ORANGE;
|
||||
|
||||
use super::Header;
|
||||
|
||||
const ONE_KB: f64 = 1000.0;
|
||||
@@ -48,6 +50,9 @@ impl PartialOrd for ContainerId {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Contains {
|
||||
fn contains(&self, input: &str) -> bool;
|
||||
}
|
||||
/// ContainerName and ContainerImage are simple structs, used so can implement custom fmt functions to them
|
||||
macro_rules! unit_struct {
|
||||
($name:ident) => {
|
||||
@@ -67,7 +72,7 @@ macro_rules! unit_struct {
|
||||
}
|
||||
}
|
||||
|
||||
impl$name {
|
||||
impl $name {
|
||||
pub fn get(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
@@ -75,26 +80,20 @@ macro_rules! unit_struct {
|
||||
pub fn set(&mut self, value: String) {
|
||||
self.0 = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, term: &str) -> bool {
|
||||
self.0.to_lowercase().contains(term)
|
||||
impl Contains for $name {
|
||||
fn contains(&self, input: &str) -> bool {
|
||||
self.0.to_lowercase().contains(input)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for $name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
if self.0.chars().count() >= 30 {
|
||||
write!(
|
||||
f,
|
||||
"{}…",
|
||||
self.0.chars().take(29).collect::<String>()
|
||||
)
|
||||
write!(f, "{}…", self.0.chars().take(29).collect::<String>())
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
self.0
|
||||
)
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,62 +210,108 @@ impl<T> StatefulList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// States of the container
|
||||
/// Store the containers status in a struct, so can then check for healthy/unhealthy status
|
||||
/// It's usually something like "Up 1 hour", "Exited (0) 10 hours ago", "Up 10 minutes (unhealthy)"
|
||||
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd)]
|
||||
pub struct ContainerStatus(String);
|
||||
|
||||
impl From<String> for ContainerStatus {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContainerStatus {
|
||||
/// Check if a container is unhealthy
|
||||
pub fn unhealthy(&self) -> bool {
|
||||
self.contains("(unhealthy)")
|
||||
}
|
||||
|
||||
/// Get a reference to the source string
|
||||
pub const fn get(&self) -> &String {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Contains for ContainerStatus {
|
||||
/// Check if the state contains a specific string
|
||||
fn contains(&self, item: &str) -> bool {
|
||||
self.0.to_lowercase().contains(item)
|
||||
}
|
||||
}
|
||||
|
||||
/// By default a container's running status will be healthy
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd)]
|
||||
pub enum RunningState {
|
||||
Healthy,
|
||||
Unhealthy,
|
||||
}
|
||||
/// States of the container
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum State {
|
||||
Dead,
|
||||
Exited,
|
||||
Paused,
|
||||
Removing,
|
||||
Restarting,
|
||||
Running,
|
||||
Running(RunningState),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub const fn is_alive(self) -> bool {
|
||||
matches!(self, Self::Running)
|
||||
matches!(self, Self::Running(_))
|
||||
}
|
||||
pub const fn get_color(self) -> Color {
|
||||
match self {
|
||||
Self::Paused => Color::Yellow,
|
||||
Self::Removing => Color::LightRed,
|
||||
Self::Restarting => Color::LightGreen,
|
||||
Self::Running => Color::Green,
|
||||
Self::Running(RunningState::Healthy) => Color::Green,
|
||||
Self::Running(RunningState::Unhealthy) => ORANGE,
|
||||
_ => Color::Red,
|
||||
}
|
||||
}
|
||||
/// Dirty way to create order for the state, rather than impl Ord
|
||||
pub const fn order(self) -> u8 {
|
||||
match self {
|
||||
Self::Running => 0,
|
||||
Self::Paused => 1,
|
||||
Self::Restarting => 2,
|
||||
Self::Removing => 3,
|
||||
Self::Exited => 4,
|
||||
Self::Dead => 5,
|
||||
Self::Unknown => 6,
|
||||
Self::Running(RunningState::Healthy) => 0,
|
||||
Self::Running(RunningState::Unhealthy) => 1,
|
||||
Self::Paused => 2,
|
||||
Self::Restarting => 3,
|
||||
Self::Removing => 4,
|
||||
Self::Exited => 5,
|
||||
Self::Dead => 6,
|
||||
Self::Unknown => 7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for State {
|
||||
fn from(input: &str) -> Self {
|
||||
/// Need status, to check if container is unhealthy or not
|
||||
impl From<(&str, &ContainerStatus)> for State {
|
||||
fn from((input, status): (&str, &ContainerStatus)) -> Self {
|
||||
match input {
|
||||
"dead" => Self::Dead,
|
||||
"exited" => Self::Exited,
|
||||
"paused" => Self::Paused,
|
||||
"removing" => Self::Removing,
|
||||
"restarting" => Self::Restarting,
|
||||
"running" => Self::Running,
|
||||
"running" => {
|
||||
if status.unhealthy() {
|
||||
Self::Running(RunningState::Unhealthy)
|
||||
} else {
|
||||
Self::Running(RunningState::Healthy)
|
||||
}
|
||||
}
|
||||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<String>> for State {
|
||||
fn from(input: Option<String>) -> Self {
|
||||
input.map_or(Self::Unknown, |input| Self::from(input.as_str()))
|
||||
/// Again, need status, to check if container is unhealthy or not
|
||||
impl From<(Option<String>, &ContainerStatus)> for State {
|
||||
fn from((input, status): (Option<String>, &ContainerStatus)) -> Self {
|
||||
input.map_or(Self::Unknown, |input| Self::from((input.as_str(), status)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +323,8 @@ impl fmt::Display for State {
|
||||
Self::Paused => "॥ paused",
|
||||
Self::Removing => "removing",
|
||||
Self::Restarting => "↻ restarting",
|
||||
Self::Running => "✓ running",
|
||||
Self::Running(RunningState::Healthy) => "✓ running",
|
||||
Self::Running(RunningState::Unhealthy) => "! running",
|
||||
Self::Unknown => "? unknown",
|
||||
};
|
||||
write!(f, "{disp}")
|
||||
@@ -314,7 +360,7 @@ impl DockerControls {
|
||||
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
|
||||
State::Paused => vec![Self::Resume, Self::Stop, Self::Delete],
|
||||
State::Restarting => vec![Self::Stop, Self::Delete],
|
||||
State::Running => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete],
|
||||
State::Running(_) => vec![Self::Pause, Self::Restart, Self::Stop, Self::Delete],
|
||||
_ => vec![Self::Delete],
|
||||
}
|
||||
}
|
||||
@@ -543,7 +589,7 @@ pub struct ContainerItem {
|
||||
pub ports: Vec<ContainerPorts>,
|
||||
pub rx: ByteStats,
|
||||
pub state: State,
|
||||
pub status: String,
|
||||
pub status: ContainerStatus,
|
||||
pub tx: ByteStats,
|
||||
}
|
||||
|
||||
@@ -572,7 +618,7 @@ impl ContainerItem {
|
||||
name: String,
|
||||
ports: Vec<ContainerPorts>,
|
||||
state: State,
|
||||
status: String,
|
||||
status: ContainerStatus,
|
||||
) -> Self {
|
||||
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
|
||||
docker_controls.start();
|
||||
@@ -686,11 +732,11 @@ mod tests {
|
||||
use ratatui::widgets::ListItem;
|
||||
|
||||
use crate::{
|
||||
app_data::{ContainerImage, Logs},
|
||||
app_data::{ContainerImage, Logs, RunningState},
|
||||
ui::log_sanitizer,
|
||||
};
|
||||
|
||||
use super::{ByteStats, ContainerName, CpuStats, LogsTz};
|
||||
use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, LogsTz, State};
|
||||
|
||||
#[test]
|
||||
/// Display CpuStats as a string
|
||||
@@ -774,4 +820,55 @@ mod tests {
|
||||
|
||||
assert_eq!(logs.logs.items.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// check ContainerStatus unhealthy state
|
||||
fn test_container_state_unhealthy() {
|
||||
let input = ContainerStatus::from("Up 1 hour".to_owned());
|
||||
|
||||
assert!(!input.unhealthy());
|
||||
|
||||
let input = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned());
|
||||
|
||||
assert!(input.unhealthy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Generate container State from a &str and &ContainerStatus
|
||||
fn test_container_status_unhealthy() {
|
||||
let healthy = ContainerStatus::from("Up 1 hour".to_owned());
|
||||
let unhealthy = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned());
|
||||
|
||||
// Running and healthy
|
||||
let input = State::from(("running", &healthy));
|
||||
assert_eq!(input, State::Running(RunningState::Healthy));
|
||||
|
||||
// Running and unhealthy
|
||||
let input = State::from(("running", &unhealthy));
|
||||
assert_eq!(input, State::Running(RunningState::Unhealthy));
|
||||
|
||||
// Dead
|
||||
let input = State::from(("dead", &healthy));
|
||||
assert_eq!(input, State::Dead);
|
||||
|
||||
// Exited
|
||||
let input = State::from(("exited", &healthy));
|
||||
assert_eq!(input, State::Exited);
|
||||
|
||||
// Paused
|
||||
let input = State::from(("paused", &healthy));
|
||||
assert_eq!(input, State::Paused);
|
||||
|
||||
// Removing
|
||||
let input = State::from(("removing", &healthy));
|
||||
assert_eq!(input, State::Removing);
|
||||
|
||||
// Restarting
|
||||
let input = State::from(("restarting", &healthy));
|
||||
assert_eq!(input, State::Restarting);
|
||||
|
||||
// Unknown
|
||||
let input = State::from(("oxker", &healthy));
|
||||
assert_eq!(input, State::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-28
@@ -179,11 +179,11 @@ impl AppData {
|
||||
FilterBy::All => {
|
||||
container.name.contains(&term)
|
||||
|| container.image.contains(&term)
|
||||
|| container.status.to_lowercase().contains(&term)
|
||||
|| container.status.contains(&term)
|
||||
}
|
||||
FilterBy::Image => container.image.contains(&term),
|
||||
FilterBy::Name => container.name.contains(&term),
|
||||
FilterBy::Status => container.status.to_lowercase().contains(&term),
|
||||
FilterBy::Status => container.status.contains(&term),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -335,7 +335,8 @@ impl AppData {
|
||||
Header::Status => item_ord
|
||||
.0
|
||||
.status
|
||||
.cmp(&item_ord.1.status)
|
||||
.get()
|
||||
.cmp(item_ord.1.status.get())
|
||||
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
|
||||
Header::Cpu => item_ord
|
||||
.0
|
||||
@@ -727,7 +728,7 @@ impl AppData {
|
||||
columns.net_rx.1 = columns.net_rx.1.max(count(&container.rx.to_string()));
|
||||
columns.net_tx.1 = columns.net_tx.1.max(count(&container.tx.to_string()));
|
||||
columns.state.1 = columns.state.1.max(count(&container.state.to_string()));
|
||||
columns.status.1 = columns.status.1.max(count(&container.status));
|
||||
columns.status.1 = columns.status.1.max(count(container.status.get()));
|
||||
}
|
||||
}
|
||||
columns
|
||||
@@ -836,12 +837,13 @@ impl AppData {
|
||||
.as_ref()
|
||||
.map_or(false, |i| i.starts_with(ENTRY_POINT));
|
||||
|
||||
let state = State::from(i.state.as_ref().map_or("dead", |z| z));
|
||||
let status = i
|
||||
.status
|
||||
.as_ref()
|
||||
.map_or(String::new(), std::clone::Clone::clone);
|
||||
let status = ContainerStatus::from(
|
||||
i.status
|
||||
.as_ref()
|
||||
.map_or(String::new(), std::clone::Clone::clone),
|
||||
);
|
||||
|
||||
let state = State::from((i.state.as_ref().map_or("dead", |z| z), &status));
|
||||
let image = i
|
||||
.image
|
||||
.as_ref()
|
||||
@@ -983,7 +985,7 @@ mod tests {
|
||||
i.state = State::Exited;
|
||||
}
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
|
||||
i.state = State::Running;
|
||||
i.state = State::Running(RunningState::Healthy);
|
||||
}
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
|
||||
i.state = State::Paused;
|
||||
@@ -1017,11 +1019,12 @@ mod tests {
|
||||
assert_eq!(result, &containers);
|
||||
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
|
||||
"Exited (0) 10 minutes ago".clone_into(&mut i.status);
|
||||
ContainerStatus::from("Exited (0) 10 minutes ago".to_owned()).clone_into(&mut i.status);
|
||||
}
|
||||
|
||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
|
||||
"Up 2 hours (Paused)".clone_into(&mut i.status);
|
||||
// "Up 2 hours (Paused)".clone_into(&mut i.status);
|
||||
ContainerStatus::from("Up 2 hours (Paused)".to_owned()).clone_into(&mut i.status);
|
||||
}
|
||||
|
||||
// Sort by status
|
||||
@@ -1342,7 +1345,7 @@ mod tests {
|
||||
result,
|
||||
Some((
|
||||
ContainerId::from("1"),
|
||||
State::Running,
|
||||
State::Running(RunningState::Healthy),
|
||||
"container_1".to_owned()
|
||||
))
|
||||
);
|
||||
@@ -1356,7 +1359,7 @@ mod tests {
|
||||
result,
|
||||
Some((
|
||||
ContainerId::from("1"),
|
||||
State::Running,
|
||||
State::Running(RunningState::Healthy),
|
||||
"container_1".to_owned()
|
||||
))
|
||||
);
|
||||
@@ -1384,7 +1387,7 @@ mod tests {
|
||||
result,
|
||||
Some((
|
||||
ContainerId::from("2"),
|
||||
State::Running,
|
||||
State::Running(RunningState::Healthy),
|
||||
"container_2".to_owned()
|
||||
))
|
||||
);
|
||||
@@ -1409,7 +1412,7 @@ mod tests {
|
||||
result,
|
||||
Some((
|
||||
ContainerId::from("3"),
|
||||
State::Running,
|
||||
State::Running(RunningState::Healthy),
|
||||
"container_3".to_owned()
|
||||
))
|
||||
);
|
||||
@@ -1423,7 +1426,7 @@ mod tests {
|
||||
result,
|
||||
Some((
|
||||
ContainerId::from("3"),
|
||||
State::Running,
|
||||
State::Running(RunningState::Healthy),
|
||||
"container_3".to_owned()
|
||||
))
|
||||
);
|
||||
@@ -1504,7 +1507,7 @@ mod tests {
|
||||
result,
|
||||
Some((
|
||||
ContainerId::from("3"),
|
||||
State::Running,
|
||||
State::Running(RunningState::Healthy),
|
||||
"container_3".to_owned()
|
||||
))
|
||||
);
|
||||
@@ -1594,7 +1597,7 @@ mod tests {
|
||||
"container_1".to_owned(),
|
||||
vec![],
|
||||
state,
|
||||
"Up 1 hour".to_owned(),
|
||||
ContainerStatus::from("Up 1 hour".to_owned()),
|
||||
)
|
||||
};
|
||||
let mut app_data = gen_appdata(&[gen_item_state(state)]);
|
||||
@@ -1635,7 +1638,7 @@ mod tests {
|
||||
&mut vec![DockerControls::Stop, DockerControls::Delete],
|
||||
);
|
||||
test_state(
|
||||
State::Running,
|
||||
State::Running(RunningState::Healthy),
|
||||
&mut vec![
|
||||
DockerControls::Pause,
|
||||
DockerControls::Restart,
|
||||
@@ -1671,7 +1674,6 @@ mod tests {
|
||||
assert_eq!(post_len, 1);
|
||||
|
||||
// Can insert checks against the current filter term
|
||||
// todo!("fix me");
|
||||
assert!(app_data.can_insert(&containers[1]));
|
||||
assert!(!app_data.can_insert(&containers[0]));
|
||||
assert!(!app_data.can_insert(&containers[2]));
|
||||
@@ -1710,7 +1712,7 @@ mod tests {
|
||||
/// Data is filtered correctly by status
|
||||
fn test_app_data_filter_by_status() {
|
||||
let (_, mut containers) = gen_containers();
|
||||
"Exited".clone_into(&mut containers[0].status);
|
||||
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
||||
let mut app_data = gen_appdata(&containers);
|
||||
|
||||
assert!(app_data.get_filter_term().is_none());
|
||||
@@ -1738,7 +1740,7 @@ mod tests {
|
||||
/// Data is filtered correctly by all
|
||||
fn test_app_data_filter_by_all() {
|
||||
let (_, mut containers) = gen_containers();
|
||||
"Exited".clone_into(&mut containers[0].status);
|
||||
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
||||
let mut app_data = gen_appdata(&containers);
|
||||
|
||||
assert!(app_data.get_filter_term().is_none());
|
||||
@@ -1767,7 +1769,7 @@ mod tests {
|
||||
/// Data is filtered correctly after various next() and previous() commands
|
||||
fn test_app_data_filter_prev() {
|
||||
let (_, mut containers) = gen_containers();
|
||||
"Exited".clone_into(&mut containers[0].status);
|
||||
ContainerStatus::from("Exited".to_owned()).clone_into(&mut containers[0].status);
|
||||
let mut app_data = gen_appdata(&containers);
|
||||
|
||||
assert!(app_data.get_filter_term().is_none());
|
||||
@@ -2067,12 +2069,12 @@ mod tests {
|
||||
(
|
||||
vec![(0.0, 1.1), (1.0, 1.2)],
|
||||
CpuStats::new(1.2),
|
||||
State::Running
|
||||
State::Running(RunningState::Healthy),
|
||||
),
|
||||
(
|
||||
vec![(0.0, 1.0), (1.0, 2.0)],
|
||||
ByteStats::new(2),
|
||||
State::Running
|
||||
State::Running(RunningState::Healthy),
|
||||
)
|
||||
))
|
||||
);
|
||||
@@ -2188,7 +2190,7 @@ mod tests {
|
||||
public: None
|
||||
}
|
||||
],
|
||||
State::Running
|
||||
State::Running(RunningState::Healthy),
|
||||
))
|
||||
);
|
||||
|
||||
@@ -2197,7 +2199,10 @@ mod tests {
|
||||
app_data.containers.items[0].ports = vec![];
|
||||
let result = app_data.get_selected_ports();
|
||||
|
||||
assert_eq!(result, Some((vec![], State::Running)));
|
||||
assert_eq!(
|
||||
result,
|
||||
Some((vec![], State::Running(RunningState::Healthy)))
|
||||
);
|
||||
}
|
||||
|
||||
// ************** //
|
||||
|
||||
Reference in New Issue
Block a user