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:
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 41 KiB |
@@ -168,10 +168,10 @@ cargo test
|
|||||||
|
|
||||||
Run some example docker images
|
Run some example docker images
|
||||||
|
|
||||||
using docker-compose.yml;
|
using docker/docker-compose.yml;
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker compose -f docker-compose.yml up -d
|
docker compose -f ./docker/docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
or individually
|
or individually
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Use an official lightweight image as a base
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install a simple utility (e.g., curl) to run as a health check
|
||||||
|
RUN apk --no-cache add curl
|
||||||
|
|
||||||
|
# Create a dummy file that we will use in our health check
|
||||||
|
RUN touch /tmp/healthy
|
||||||
|
|
||||||
|
# Define a simple health check
|
||||||
|
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
|
||||||
|
CMD [ ! -f /tmp/healthy ] || exit 1
|
||||||
|
|
||||||
|
# Start a basic loop that keeps the container running
|
||||||
|
CMD ["sh", "-c", "while :; do echo 'Container is running but will be unhealthy'; sleep 30; done"]
|
||||||
|
|
||||||
|
# docker build -t unhealthy-container . -f Dockerfile.unhealthy; docker run -d --name unhealthy unhealthy-container
|
||||||
@@ -39,5 +39,20 @@ services:
|
|||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 512M
|
memory: 512M
|
||||||
|
some_container:
|
||||||
|
container_name: some_container
|
||||||
|
image: some_container
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.unhealthy
|
||||||
|
ipc: private
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- oxker-example-net
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 128M
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
+132
-35
@@ -10,6 +10,8 @@ use ratatui::{
|
|||||||
widgets::{ListItem, ListState},
|
widgets::{ListItem, ListState},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::ui::ORANGE;
|
||||||
|
|
||||||
use super::Header;
|
use super::Header;
|
||||||
|
|
||||||
const ONE_KB: f64 = 1000.0;
|
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
|
/// ContainerName and ContainerImage are simple structs, used so can implement custom fmt functions to them
|
||||||
macro_rules! unit_struct {
|
macro_rules! unit_struct {
|
||||||
($name:ident) => {
|
($name:ident) => {
|
||||||
@@ -75,26 +80,20 @@ macro_rules! unit_struct {
|
|||||||
pub fn set(&mut self, value: String) {
|
pub fn set(&mut self, value: String) {
|
||||||
self.0 = value;
|
self.0 = value;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn contains(&self, term: &str) -> bool {
|
impl Contains for $name {
|
||||||
self.0.to_lowercase().contains(term)
|
fn contains(&self, input: &str) -> bool {
|
||||||
|
self.0.to_lowercase().contains(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for $name {
|
impl std::fmt::Display for $name {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
if self.0.chars().count() >= 30 {
|
if self.0.chars().count() >= 30 {
|
||||||
write!(
|
write!(f, "{}…", self.0.chars().take(29).collect::<String>())
|
||||||
f,
|
|
||||||
"{}…",
|
|
||||||
self.0.chars().take(29).collect::<String>()
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(f, "{}", self.0)
|
||||||
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)]
|
#[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 {
|
pub enum State {
|
||||||
Dead,
|
Dead,
|
||||||
Exited,
|
Exited,
|
||||||
Paused,
|
Paused,
|
||||||
Removing,
|
Removing,
|
||||||
Restarting,
|
Restarting,
|
||||||
Running,
|
Running(RunningState),
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub const fn is_alive(self) -> bool {
|
pub const fn is_alive(self) -> bool {
|
||||||
matches!(self, Self::Running)
|
matches!(self, Self::Running(_))
|
||||||
}
|
}
|
||||||
pub const fn get_color(self) -> Color {
|
pub const fn get_color(self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Self::Paused => Color::Yellow,
|
Self::Paused => Color::Yellow,
|
||||||
Self::Removing => Color::LightRed,
|
Self::Removing => Color::LightRed,
|
||||||
Self::Restarting => Color::LightGreen,
|
Self::Restarting => Color::LightGreen,
|
||||||
Self::Running => Color::Green,
|
Self::Running(RunningState::Healthy) => Color::Green,
|
||||||
|
Self::Running(RunningState::Unhealthy) => ORANGE,
|
||||||
_ => Color::Red,
|
_ => Color::Red,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Dirty way to create order for the state, rather than impl Ord
|
/// Dirty way to create order for the state, rather than impl Ord
|
||||||
pub const fn order(self) -> u8 {
|
pub const fn order(self) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
Self::Running => 0,
|
Self::Running(RunningState::Healthy) => 0,
|
||||||
Self::Paused => 1,
|
Self::Running(RunningState::Unhealthy) => 1,
|
||||||
Self::Restarting => 2,
|
Self::Paused => 2,
|
||||||
Self::Removing => 3,
|
Self::Restarting => 3,
|
||||||
Self::Exited => 4,
|
Self::Removing => 4,
|
||||||
Self::Dead => 5,
|
Self::Exited => 5,
|
||||||
Self::Unknown => 6,
|
Self::Dead => 6,
|
||||||
|
Self::Unknown => 7,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for State {
|
/// Need status, to check if container is unhealthy or not
|
||||||
fn from(input: &str) -> Self {
|
impl From<(&str, &ContainerStatus)> for State {
|
||||||
|
fn from((input, status): (&str, &ContainerStatus)) -> Self {
|
||||||
match input {
|
match input {
|
||||||
"dead" => Self::Dead,
|
"dead" => Self::Dead,
|
||||||
"exited" => Self::Exited,
|
"exited" => Self::Exited,
|
||||||
"paused" => Self::Paused,
|
"paused" => Self::Paused,
|
||||||
"removing" => Self::Removing,
|
"removing" => Self::Removing,
|
||||||
"restarting" => Self::Restarting,
|
"restarting" => Self::Restarting,
|
||||||
"running" => Self::Running,
|
"running" => {
|
||||||
|
if status.unhealthy() {
|
||||||
|
Self::Running(RunningState::Unhealthy)
|
||||||
|
} else {
|
||||||
|
Self::Running(RunningState::Healthy)
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => Self::Unknown,
|
_ => Self::Unknown,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<String>> for State {
|
/// Again, need status, to check if container is unhealthy or not
|
||||||
fn from(input: Option<String>) -> Self {
|
impl From<(Option<String>, &ContainerStatus)> for State {
|
||||||
input.map_or(Self::Unknown, |input| Self::from(input.as_str()))
|
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::Paused => "॥ paused",
|
||||||
Self::Removing => "removing",
|
Self::Removing => "removing",
|
||||||
Self::Restarting => "↻ restarting",
|
Self::Restarting => "↻ restarting",
|
||||||
Self::Running => "✓ running",
|
Self::Running(RunningState::Healthy) => "✓ running",
|
||||||
|
Self::Running(RunningState::Unhealthy) => "! running",
|
||||||
Self::Unknown => "? unknown",
|
Self::Unknown => "? unknown",
|
||||||
};
|
};
|
||||||
write!(f, "{disp}")
|
write!(f, "{disp}")
|
||||||
@@ -314,7 +360,7 @@ impl DockerControls {
|
|||||||
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
|
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
|
||||||
State::Paused => vec![Self::Resume, Self::Stop, Self::Delete],
|
State::Paused => vec![Self::Resume, Self::Stop, Self::Delete],
|
||||||
State::Restarting => vec![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],
|
_ => vec![Self::Delete],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -543,7 +589,7 @@ pub struct ContainerItem {
|
|||||||
pub ports: Vec<ContainerPorts>,
|
pub ports: Vec<ContainerPorts>,
|
||||||
pub rx: ByteStats,
|
pub rx: ByteStats,
|
||||||
pub state: State,
|
pub state: State,
|
||||||
pub status: String,
|
pub status: ContainerStatus,
|
||||||
pub tx: ByteStats,
|
pub tx: ByteStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,7 +618,7 @@ impl ContainerItem {
|
|||||||
name: String,
|
name: String,
|
||||||
ports: Vec<ContainerPorts>,
|
ports: Vec<ContainerPorts>,
|
||||||
state: State,
|
state: State,
|
||||||
status: String,
|
status: ContainerStatus,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
|
let mut docker_controls = StatefulList::new(DockerControls::gen_vec(state));
|
||||||
docker_controls.start();
|
docker_controls.start();
|
||||||
@@ -686,11 +732,11 @@ mod tests {
|
|||||||
use ratatui::widgets::ListItem;
|
use ratatui::widgets::ListItem;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{ContainerImage, Logs},
|
app_data::{ContainerImage, Logs, RunningState},
|
||||||
ui::log_sanitizer,
|
ui::log_sanitizer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{ByteStats, ContainerName, CpuStats, LogsTz};
|
use super::{ByteStats, ContainerName, ContainerStatus, CpuStats, LogsTz, State};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Display CpuStats as a string
|
/// Display CpuStats as a string
|
||||||
@@ -774,4 +820,55 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(logs.logs.items.len(), 2);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-27
@@ -179,11 +179,11 @@ impl AppData {
|
|||||||
FilterBy::All => {
|
FilterBy::All => {
|
||||||
container.name.contains(&term)
|
container.name.contains(&term)
|
||||||
|| container.image.contains(&term)
|
|| container.image.contains(&term)
|
||||||
|| container.status.to_lowercase().contains(&term)
|
|| container.status.contains(&term)
|
||||||
}
|
}
|
||||||
FilterBy::Image => container.image.contains(&term),
|
FilterBy::Image => container.image.contains(&term),
|
||||||
FilterBy::Name => container.name.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
|
Header::Status => item_ord
|
||||||
.0
|
.0
|
||||||
.status
|
.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())),
|
.then_with(|| item_ord.0.name.get().cmp(item_ord.1.name.get())),
|
||||||
Header::Cpu => item_ord
|
Header::Cpu => item_ord
|
||||||
.0
|
.0
|
||||||
@@ -727,7 +728,7 @@ impl AppData {
|
|||||||
columns.net_rx.1 = columns.net_rx.1.max(count(&container.rx.to_string()));
|
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.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.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
|
columns
|
||||||
@@ -836,12 +837,13 @@ impl AppData {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |i| i.starts_with(ENTRY_POINT));
|
.map_or(false, |i| i.starts_with(ENTRY_POINT));
|
||||||
|
|
||||||
let state = State::from(i.state.as_ref().map_or("dead", |z| z));
|
let status = ContainerStatus::from(
|
||||||
let status = i
|
i.status
|
||||||
.status
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(String::new(), std::clone::Clone::clone);
|
.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
|
let image = i
|
||||||
.image
|
.image
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -983,7 +985,7 @@ mod tests {
|
|||||||
i.state = State::Exited;
|
i.state = State::Exited;
|
||||||
}
|
}
|
||||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
|
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")) {
|
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) {
|
||||||
i.state = State::Paused;
|
i.state = State::Paused;
|
||||||
@@ -1017,11 +1019,12 @@ mod tests {
|
|||||||
assert_eq!(result, &containers);
|
assert_eq!(result, &containers);
|
||||||
|
|
||||||
if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) {
|
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")) {
|
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
|
// Sort by status
|
||||||
@@ -1342,7 +1345,7 @@ mod tests {
|
|||||||
result,
|
result,
|
||||||
Some((
|
Some((
|
||||||
ContainerId::from("1"),
|
ContainerId::from("1"),
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
"container_1".to_owned()
|
"container_1".to_owned()
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -1356,7 +1359,7 @@ mod tests {
|
|||||||
result,
|
result,
|
||||||
Some((
|
Some((
|
||||||
ContainerId::from("1"),
|
ContainerId::from("1"),
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
"container_1".to_owned()
|
"container_1".to_owned()
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -1384,7 +1387,7 @@ mod tests {
|
|||||||
result,
|
result,
|
||||||
Some((
|
Some((
|
||||||
ContainerId::from("2"),
|
ContainerId::from("2"),
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
"container_2".to_owned()
|
"container_2".to_owned()
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -1409,7 +1412,7 @@ mod tests {
|
|||||||
result,
|
result,
|
||||||
Some((
|
Some((
|
||||||
ContainerId::from("3"),
|
ContainerId::from("3"),
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
"container_3".to_owned()
|
"container_3".to_owned()
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -1423,7 +1426,7 @@ mod tests {
|
|||||||
result,
|
result,
|
||||||
Some((
|
Some((
|
||||||
ContainerId::from("3"),
|
ContainerId::from("3"),
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
"container_3".to_owned()
|
"container_3".to_owned()
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -1504,7 +1507,7 @@ mod tests {
|
|||||||
result,
|
result,
|
||||||
Some((
|
Some((
|
||||||
ContainerId::from("3"),
|
ContainerId::from("3"),
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
"container_3".to_owned()
|
"container_3".to_owned()
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -1594,7 +1597,7 @@ mod tests {
|
|||||||
"container_1".to_owned(),
|
"container_1".to_owned(),
|
||||||
vec![],
|
vec![],
|
||||||
state,
|
state,
|
||||||
"Up 1 hour".to_owned(),
|
ContainerStatus::from("Up 1 hour".to_owned()),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let mut app_data = gen_appdata(&[gen_item_state(state)]);
|
let mut app_data = gen_appdata(&[gen_item_state(state)]);
|
||||||
@@ -1635,7 +1638,7 @@ mod tests {
|
|||||||
&mut vec![DockerControls::Stop, DockerControls::Delete],
|
&mut vec![DockerControls::Stop, DockerControls::Delete],
|
||||||
);
|
);
|
||||||
test_state(
|
test_state(
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
&mut vec![
|
&mut vec![
|
||||||
DockerControls::Pause,
|
DockerControls::Pause,
|
||||||
DockerControls::Restart,
|
DockerControls::Restart,
|
||||||
@@ -1671,7 +1674,6 @@ mod tests {
|
|||||||
assert_eq!(post_len, 1);
|
assert_eq!(post_len, 1);
|
||||||
|
|
||||||
// Can insert checks against the current filter term
|
// Can insert checks against the current filter term
|
||||||
// todo!("fix me");
|
|
||||||
assert!(app_data.can_insert(&containers[1]));
|
assert!(app_data.can_insert(&containers[1]));
|
||||||
assert!(!app_data.can_insert(&containers[0]));
|
assert!(!app_data.can_insert(&containers[0]));
|
||||||
assert!(!app_data.can_insert(&containers[2]));
|
assert!(!app_data.can_insert(&containers[2]));
|
||||||
@@ -1710,7 +1712,7 @@ mod tests {
|
|||||||
/// Data is filtered correctly by status
|
/// Data is filtered correctly by status
|
||||||
fn test_app_data_filter_by_status() {
|
fn test_app_data_filter_by_status() {
|
||||||
let (_, mut containers) = gen_containers();
|
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);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
assert!(app_data.get_filter_term().is_none());
|
assert!(app_data.get_filter_term().is_none());
|
||||||
@@ -1738,7 +1740,7 @@ mod tests {
|
|||||||
/// Data is filtered correctly by all
|
/// Data is filtered correctly by all
|
||||||
fn test_app_data_filter_by_all() {
|
fn test_app_data_filter_by_all() {
|
||||||
let (_, mut containers) = gen_containers();
|
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);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
assert!(app_data.get_filter_term().is_none());
|
assert!(app_data.get_filter_term().is_none());
|
||||||
@@ -1767,7 +1769,7 @@ mod tests {
|
|||||||
/// Data is filtered correctly after various next() and previous() commands
|
/// Data is filtered correctly after various next() and previous() commands
|
||||||
fn test_app_data_filter_prev() {
|
fn test_app_data_filter_prev() {
|
||||||
let (_, mut containers) = gen_containers();
|
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);
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
assert!(app_data.get_filter_term().is_none());
|
assert!(app_data.get_filter_term().is_none());
|
||||||
@@ -2067,12 +2069,12 @@ mod tests {
|
|||||||
(
|
(
|
||||||
vec![(0.0, 1.1), (1.0, 1.2)],
|
vec![(0.0, 1.1), (1.0, 1.2)],
|
||||||
CpuStats::new(1.2),
|
CpuStats::new(1.2),
|
||||||
State::Running
|
State::Running(RunningState::Healthy),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
vec![(0.0, 1.0), (1.0, 2.0)],
|
vec![(0.0, 1.0), (1.0, 2.0)],
|
||||||
ByteStats::new(2),
|
ByteStats::new(2),
|
||||||
State::Running
|
State::Running(RunningState::Healthy),
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
@@ -2188,7 +2190,7 @@ mod tests {
|
|||||||
public: None
|
public: None
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
State::Running
|
State::Running(RunningState::Healthy),
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2197,7 +2199,10 @@ mod tests {
|
|||||||
app_data.containers.items[0].ports = vec![];
|
app_data.containers.items[0].ports = vec![];
|
||||||
let result = app_data.get_selected_ports();
|
let result = app_data.get_selected_ports();
|
||||||
|
|
||||||
assert_eq!(result, Some((vec![], State::Running)));
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Some((vec![], State::Running(RunningState::Healthy)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ************** //
|
// ************** //
|
||||||
|
|||||||
+10
-2
@@ -22,7 +22,7 @@ use tokio::{
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ContainerId, DockerControls, State},
|
app_data::{AppData, ContainerId, ContainerStatus, DockerControls, State},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
parse_args::CliArgs,
|
parse_args::CliArgs,
|
||||||
ui::{GuiState, Status},
|
ui::{GuiState, Status},
|
||||||
@@ -236,7 +236,15 @@ impl DockerData {
|
|||||||
output
|
output
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|i| {
|
.filter_map(|i| {
|
||||||
i.id.map(|id| (State::from(i.state), ContainerId::from(id.as_str())))
|
i.id.map(|id| {
|
||||||
|
(
|
||||||
|
State::from((
|
||||||
|
i.state,
|
||||||
|
&ContainerStatus::from(i.status.map_or_else(String::new, |i| i)),
|
||||||
|
)),
|
||||||
|
ContainerId::from(id.as_str()),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-2
@@ -18,7 +18,7 @@ use tokio::{
|
|||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{AppData, ContainerId, State},
|
app_data::{AppData, ContainerId, RunningState, State},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,7 +162,12 @@ impl ExecMode {
|
|||||||
let container = app_data.lock().get_selected_container_id_state_name();
|
let container = app_data.lock().get_selected_container_id_state_name();
|
||||||
|
|
||||||
if let Some((id, state, _)) = container {
|
if let Some((id, state, _)) = container {
|
||||||
if state == State::Running {
|
if [
|
||||||
|
State::Running(RunningState::Healthy),
|
||||||
|
State::Running(RunningState::Unhealthy),
|
||||||
|
]
|
||||||
|
.contains(&state)
|
||||||
|
{
|
||||||
if tty_readable() && !use_cli {
|
if tty_readable() && !use_cli {
|
||||||
if let Ok(exec) = docker
|
if let Ok(exec) = docker
|
||||||
.create_exec(
|
.create_exec(
|
||||||
|
|||||||
+4
-3
@@ -176,7 +176,8 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{
|
app_data::{
|
||||||
AppData, ContainerId, ContainerItem, ContainerPorts, Filter, State, StatefulList,
|
AppData, ContainerId, ContainerItem, ContainerPorts, ContainerStatus, Filter,
|
||||||
|
RunningState, State, StatefulList,
|
||||||
},
|
},
|
||||||
parse_args::CliArgs,
|
parse_args::CliArgs,
|
||||||
};
|
};
|
||||||
@@ -208,8 +209,8 @@ mod tests {
|
|||||||
private: u16::try_from(index).unwrap_or(1) + 8000,
|
private: u16::try_from(index).unwrap_or(1) + 8000,
|
||||||
public: None,
|
public: None,
|
||||||
}],
|
}],
|
||||||
State::Running,
|
State::Running(RunningState::Healthy),
|
||||||
format!("Up {index} hour"),
|
ContainerStatus::from(format!("Up {index} hour")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+68
-7
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
gui_state::{BoxLocation, DeleteButton, Region},
|
gui_state::{BoxLocation, DeleteButton, Region},
|
||||||
FrameData, Status,
|
FrameData, Status, ORANGE,
|
||||||
};
|
};
|
||||||
use super::{GuiState, SelectablePanel};
|
use super::{GuiState, SelectablePanel};
|
||||||
|
|
||||||
@@ -39,7 +39,6 @@ const NAME: &str = env!("CARGO_PKG_NAME");
|
|||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
||||||
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||||
const ORANGE: Color = Color::Rgb(255, 178, 36);
|
|
||||||
const MARGIN: &str = " ";
|
const MARGIN: &str = " ";
|
||||||
const RIGHT_ARROW: &str = "▶ ";
|
const RIGHT_ARROW: &str = "▶ ";
|
||||||
const CIRCLE: &str = "⚪ ";
|
const CIRCLE: &str = "⚪ ";
|
||||||
@@ -163,7 +162,7 @@ fn format_containers<'a>(i: &ContainerItem, widths: &Columns) -> Line<'a> {
|
|||||||
Span::styled(
|
Span::styled(
|
||||||
format!(
|
format!(
|
||||||
"{:<width$}{MARGIN}",
|
"{:<width$}{MARGIN}",
|
||||||
i.status,
|
i.status.get(),
|
||||||
width = &widths.status.1.into()
|
width = &widths.status.1.into()
|
||||||
),
|
),
|
||||||
state_style,
|
state_style,
|
||||||
@@ -311,7 +310,7 @@ pub fn ports(
|
|||||||
|
|
||||||
if ports.0.is_empty() {
|
if ports.0.is_empty() {
|
||||||
let text = match ports.1 {
|
let text = match ports.1 {
|
||||||
State::Running | State::Paused | State::Restarting => "no ports",
|
State::Running(_) | State::Paused | State::Restarting => "no ports",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD))
|
let paragraph = Paragraph::new(Span::from(text).add_modifier(Modifier::BOLD))
|
||||||
@@ -383,7 +382,7 @@ fn make_chart<'a, T: Stats + Display>(
|
|||||||
) -> Chart<'a> {
|
) -> Chart<'a> {
|
||||||
let title_color = state.get_color();
|
let title_color = state.get_color();
|
||||||
let label_color = match state {
|
let label_color = match state {
|
||||||
State::Running => ORANGE,
|
State::Running(_) => ORANGE,
|
||||||
_ => state.get_color(),
|
_ => state.get_color(),
|
||||||
};
|
};
|
||||||
Chart::new(dataset)
|
Chart::new(dataset)
|
||||||
@@ -1081,8 +1080,8 @@ mod tests {
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_data::{
|
app_data::{
|
||||||
AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, Header,
|
AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, ContainerStatus,
|
||||||
SortedOrder, State, StatefulList,
|
Header, SortedOrder, State, StatefulList,
|
||||||
},
|
},
|
||||||
app_error::AppError,
|
app_error::AppError,
|
||||||
tests::{gen_appdata, gen_container_summary, gen_containers},
|
tests::{gen_appdata, gen_container_summary, gen_containers},
|
||||||
@@ -1782,6 +1781,68 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// When container state is unknown, correct colors displayed
|
||||||
|
fn test_draw_blocks_containers_unhealthy() {
|
||||||
|
let (w, h) = (130, 6);
|
||||||
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
|
let status = ContainerStatus::from("Up 1 hour (unhealthy)".to_owned());
|
||||||
|
setup.app_data.lock().containers.items[0].state = State::from(("running", &status));
|
||||||
|
setup.app_data.lock().containers.items[0].status = status;
|
||||||
|
|
||||||
|
let expected= [
|
||||||
|
"╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
|
||||||
|
"│⚪ container_1 ! running Up 1 hour (unhealthy) 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │",
|
||||||
|
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │",
|
||||||
|
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │",
|
||||||
|
"│ │",
|
||||||
|
"╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
|
];
|
||||||
|
let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock()));
|
||||||
|
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for (row_index, result_row) in get_result(&setup, w) {
|
||||||
|
let expected_row = expected_to_vec(&expected, row_index);
|
||||||
|
for (result_cell_index, result_cell) in result_row.iter().enumerate() {
|
||||||
|
assert_eq!(result_cell.symbol(), expected_row[result_cell_index]);
|
||||||
|
match (row_index, result_cell_index) {
|
||||||
|
// border
|
||||||
|
(0 | 5, _) | (1..=4, 0 | 129) => {
|
||||||
|
assert_eq!(result_cell.fg, Color::LightCyan);
|
||||||
|
}
|
||||||
|
// name, id, image column
|
||||||
|
(1..=3, 4..=17 | 83..=103) => {
|
||||||
|
assert_eq!(result_cell.fg, Color::Blue);
|
||||||
|
}
|
||||||
|
// state, status, cpu, memory column of the first row
|
||||||
|
(1, 18..=82) => {
|
||||||
|
assert_eq!(result_cell.fg, ORANGE);
|
||||||
|
}
|
||||||
|
// state, status, cpu, memory column
|
||||||
|
(2..=3, 18..=82) => {
|
||||||
|
assert_eq!(result_cell.fg, Color::Green);
|
||||||
|
}
|
||||||
|
// rx column
|
||||||
|
(1..=3, 104..=113) => {
|
||||||
|
assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193));
|
||||||
|
}
|
||||||
|
// tx column
|
||||||
|
(1..=3, 114..=123) => {
|
||||||
|
assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140));
|
||||||
|
}
|
||||||
|
_ => assert_eq!(result_cell.fg, Color::Reset),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// When container state is unknown, correct colors displayed
|
/// When container state is unknown, correct colors displayed
|
||||||
fn test_draw_blocks_containers_unknown() {
|
fn test_draw_blocks_containers_unknown() {
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ use crate::{
|
|||||||
input_handler::InputMessages,
|
input_handler::InputMessages,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const ORANGE: ratatui::style::Color = ratatui::style::Color::Rgb(255, 178, 36);
|
||||||
|
|
||||||
pub struct Ui {
|
pub struct Ui {
|
||||||
app_data: Arc<Mutex<AppData>>,
|
app_data: Arc<Mutex<AppData>>,
|
||||||
gui_state: Arc<Mutex<GuiState>>,
|
gui_state: Arc<Mutex<GuiState>>,
|
||||||
|
|||||||
Reference in New Issue
Block a user