chore: merge release-v0.7.0 into main

This commit is contained in:
Jack Wills
2024-08-01 21:59:46 +00:00
22 changed files with 2529 additions and 1433 deletions
+1 -1
View File
@@ -25,11 +25,11 @@
"extensions": [ "extensions": [
"bmuskalla.vscode-tldr", "bmuskalla.vscode-tldr",
"christian-kohler.path-intellisense", "christian-kohler.path-intellisense",
"fill-labs.dependi",
"foxundermoon.shell-format", "foxundermoon.shell-format",
"mutantdino.resourcemonitor", "mutantdino.resourcemonitor",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"rust-lang.rust-analyzer", "rust-lang.rust-analyzer",
"serayuzgur.crates",
"tamasfe.even-better-toml", "tamasfe.even-better-toml",
"timonwong.shellcheck", "timonwong.shellcheck",
"vadimcn.vscode-lldb" "vadimcn.vscode-lldb"
+21 -4
View File
@@ -1,12 +1,29 @@
### 2024-05-25 ### 2024-08-01
### Chores ### Chores
+ Dependencies updated, [51fdd26be5b3166bcff5c26ece6d6ec0d893381e], [c1be658b8cc4786a9a7f2e0a88568019b3995c14] + .devcontainer extensions updated, [0288cbc8146cde1dd40ceaec9550198b635bb8f5]
+ dependencies updated, [1df4f78dc41013c33d901925933b1ccb29ad4bc8], [5ae253b8734ba0495e4e8149b17d5228b3d86f8d], [7a517db9f7c14c35e56ff70cf76ffb608fd30e17], [9c291cd9c81b6d9a02085878588ed3b845fd0046], [0e90f4eb55ac5fb5d45e7d212c3686027dd3913e], [fe71cbfb00f166b7c02a6e28e64650ed1b47d15d]
+ docker-compose alpine version bump, [51ceab3ebdb09356cd401d2f268840239255126f]
+ Rust 1.80 linting, [93e1279b1fc77019442a385e2e36be2fe438e828]
+ create_release v0.5.6, [f408acfe9a9f5a976735b8a8a51500fd7b865daf]
### Docs ### Docs
+ exec mode "not available on Windows", in both README.md and help panel, [df449a85376bbeec87215952d6a9196721f7132e] + screenshot updated, [6975ebe70f7058229c232e4a56b090f55247d2a2]
### Features
+ left align all text, [e0d421c4918a17c9e0e21fd214edb99d71281c9d]
+ place image name in logs panel title, [12f24357a68abe871f44d871d95b6e2ef062181e]
+ distinguish between unhealthy & healthy running containers, closes #43, [de8768181631c6d961ce0e4dacb50c2ed02abc36]
+ filter containers, use `F1` or `/` to enter filter mode, closes #37, thanks to [MohammadShabaniSBU](https://github.com/MohammadShabaniSBU) for the original PR, [d5d8a0dbc5437ff3b17f34b9dbb9589bb56b4a3e], [[7ee1f06f804683e3395953a02138d4e9da115ea9]]
+ place image name in logs panel title, [ef19b9cf89a881d0a7ac818885317ce2bd683dfc]
### Fixes ### Fixes
+ closes #36 Double key strokes on Windows, [9b7d575a76398cbe19e17f6494baf802dbb512b9] + log_sanitizer `raw()` & `remove_ansi()` now functioning as intended, [0dc98dfc8113869b81be9d697ca77418c919e4bf]
+ Dockerfile command use uppercase, [068e4025a5d6049a9a6951a0480a6bdef7379f88]
+ heading section help margin, [0e927aae178c1d8f60561b93607a26d45a1d9331]
+ install.sh use curl, [197a031b8cf356f49f08e04472d0d1c489699415]
### Tests
+ fix layout tests with new left alignment, [dfced564278eafdbb8a5b95badbae3a7c4bf87b3]
see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details see <a href='https://github.com/mrjackwills/oxker/blob/main/CHANGELOG.md'>CHANGELOG.md</a> for more details
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 41 KiB

+29
View File
@@ -1,3 +1,32 @@
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.7.0'>v0.7.0</a>
### 2024-08-01
### Chores
+ .devcontainer extensions updated, [0288cbc8](https://github.com/mrjackwills/oxker/commit/0288cbc8146cde1dd40ceaec9550198b635bb8f5)
+ dependencies updated, [1df4f78d](https://github.com/mrjackwills/oxker/commit/1df4f78dc41013c33d901925933b1ccb29ad4bc8), [5ae253b8](https://github.com/mrjackwills/oxker/commit/5ae253b8734ba0495e4e8149b17d5228b3d86f8d), [7a517db9](https://github.com/mrjackwills/oxker/commit/7a517db9f7c14c35e56ff70cf76ffb608fd30e17), [9c291cd9](https://github.com/mrjackwills/oxker/commit/9c291cd9c81b6d9a02085878588ed3b845fd0046), [0e90f4eb](https://github.com/mrjackwills/oxker/commit/0e90f4eb55ac5fb5d45e7d212c3686027dd3913e), [fe71cbfb](https://github.com/mrjackwills/oxker/commit/fe71cbfb00f166b7c02a6e28e64650ed1b47d15d)
+ docker-compose alpine version bump, [51ceab3e](https://github.com/mrjackwills/oxker/commit/51ceab3ebdb09356cd401d2f268840239255126f)
+ Rust 1.80 linting, [93e1279b](https://github.com/mrjackwills/oxker/commit/93e1279b1fc77019442a385e2e36be2fe438e828)
+ create_release v0.5.6, [f408acfe](https://github.com/mrjackwills/oxker/commit/f408acfe9a9f5a976735b8a8a51500fd7b865daf)
### Docs
+ screenshot updated, [6975ebe7](https://github.com/mrjackwills/oxker/commit/6975ebe70f7058229c232e4a56b090f55247d2a2)
### Features
+ left align all text, [e0d421c4](https://github.com/mrjackwills/oxker/commit/e0d421c4918a17c9e0e21fd214edb99d71281c9d)
+ place image name in logs panel title, [12f24357](https://github.com/mrjackwills/oxker/commit/12f24357a68abe871f44d871d95b6e2ef062181e)
+ distinguish between unhealthy & healthy running containers, closes [#43](https://github.com/mrjackwills/oxker/issues/43), [de876818](https://github.com/mrjackwills/oxker/commit/de8768181631c6d961ce0e4dacb50c2ed02abc36)
+ filter containers, use `F1` or `/` to enter filter mode, closes [#37](https://github.com/mrjackwills/oxker/issues/37), thanks to [MohammadShabaniSBU](https://github.com/MohammadShabaniSBU) for the original PR, [d5d8a0db](https://github.com/mrjackwills/oxker/commit/d5d8a0dbc5437ff3b17f34b9dbb9589bb56b4a3e), [[7ee1f06f804683e3395953a02138d4e9da115ea9]]
+ place image name in logs panel title, [ef19b9cf](https://github.com/mrjackwills/oxker/commit/ef19b9cf89a881d0a7ac818885317ce2bd683dfc)
### Fixes
+ log_sanitizer `raw()` & `remove_ansi()` now functioning as intended, [0dc98dfc](https://github.com/mrjackwills/oxker/commit/0dc98dfc8113869b81be9d697ca77418c919e4bf)
+ Dockerfile command use uppercase, [068e4025](https://github.com/mrjackwills/oxker/commit/068e4025a5d6049a9a6951a0480a6bdef7379f88)
+ heading section help margin, [0e927aae](https://github.com/mrjackwills/oxker/commit/0e927aae178c1d8f60561b93607a26d45a1d9331)
+ install.sh use curl, [197a031b](https://github.com/mrjackwills/oxker/commit/197a031b8cf356f49f08e04472d0d1c489699415)
### Tests
+ fix layout tests with new left alignment, [dfced564](https://github.com/mrjackwills/oxker/commit/dfced564278eafdbb8a5b95badbae3a7c4bf87b3)
# <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.4'>v0.6.4</a> # <a href='https://github.com/mrjackwills/oxker/releases/tag/v0.6.4'>v0.6.4</a>
### 2024-05-25 ### 2024-05-25
Generated
+266 -184
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "oxker" name = "oxker"
version = "0.6.4" version = "0.7.0"
edition = "2021" edition = "2021"
authors = ["Jack Wills <email@mrjackwills.com>"] authors = ["Jack Wills <email@mrjackwills.com>"]
description = "A simple tui to view & control docker containers" description = "A simple tui to view & control docker containers"
@@ -27,19 +27,19 @@ similar_names = "allow"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
bollard = "0.16" bollard = "0.17"
cansi = "2.2" cansi = "2.2"
clap = { version = "4.5", features = ["color", "derive", "unicode"] } clap = { version = "4.5", features = ["color", "derive", "unicode"] }
crossterm = "0.27" crossterm = "0.28"
directories = "5.0" directories = "5.0"
futures-util = "0.3" futures-util = "0.3"
parking_lot = { version = "0.12" } parking_lot = { version = "0.12" }
ratatui = "0.26" ratatui = "0.27"
tokio = { version = "1.37", features = ["full"] } tokio = { version = "1.39", features = ["full"] }
tokio-util = "0.7" tokio-util = "0.7"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = "0.3" tracing-subscriber = "0.3"
uuid = { version = "1.8", features = ["fast-rng", "v4"] } uuid = { version = "1.10", features = ["fast-rng", "v4"] }
[profile.release] [profile.release]
lto = true lto = true
+3 -2
View File
@@ -101,6 +101,7 @@ In application controls
| ```( enter )```| Run selected docker command.| | ```( enter )```| Run selected docker command.|
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. | | ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
| ```( 0 )``` | Stop sorting.| | ```( 0 )``` | Stop sorting.|
| ```( F1 )``` or ```( / )``` | Enter filter mode. |
| ```( e )``` | Exec into the selected container - not available on Windows.| | ```( e )``` | Exec into the selected container - not available on Windows.|
| ```( h )``` | Toggle help menu.| | ```( h )``` | Toggle help menu.|
| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.| | ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
@@ -167,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
+3 -3
View File
@@ -2,7 +2,7 @@
## Builder ## ## Builder ##
############# #############
FROM --platform=linux/amd64 rust:slim as BUILDER FROM --platform=linux/amd64 rust:slim AS builder
ARG TARGETARCH ARG TARGETARCH
@@ -49,12 +49,12 @@ RUN cp /usr/src/oxker/target/$(cat /.platform)/release/oxker /
## Runtime ## ## Runtime ##
############# #############
FROM scratch as RUNTIME FROM scratch
# Set an ENV to indicate that we're running in a container # Set an ENV to indicate that we're running in a container
ENV OXKER_RUNTIME=container ENV OXKER_RUNTIME=container
COPY --from=BUILDER /oxker /app/ COPY --from=builder /oxker /app/
# Run the application # Run the application
# this is used in the application itself so DO NOT EDIT # this is used in the application itself so DO NOT EDIT
+17 -16
View File
@@ -1,6 +1,7 @@
#!/bin/bash #!/bin/bash
# rust create_release v0.5.5 # rust create_release v0.5.6
# 2024-07-27
STAR_LINE='****************************************' STAR_LINE='****************************************'
CWD=$(pwd) CWD=$(pwd)
@@ -191,25 +192,25 @@ check_cross() {
fi fi
} }
cargo_build_x86_linux() { cross_build_x86_linux() {
check_cross check_cross
echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}" echo -e "${YELLOW}cross build --target x86_64-unknown-linux-musl --release${RESET}"
cross build --target x86_64-unknown-linux-musl --release cross build --target x86_64-unknown-linux-musl --release
} }
cargo_build_aarch64_linux() { cross_build_aarch64_linux() {
check_cross check_cross
echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}" echo -e "${YELLOW}cross build --target aarch64-unknown-linux-musl --release${RESET}"
cross build --target aarch64-unknown-linux-musl --release cross build --target aarch64-unknown-linux-musl --release
} }
cargo_build_armv6_linux() { cross_build_armv6_linux() {
check_cross check_cross
echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}" echo -e "${YELLOW}cross build --target arm-unknown-linux-musleabihf --release${RESET}"
cross build --target arm-unknown-linux-musleabihf --release cross build --target arm-unknown-linux-musleabihf --release
} }
cargo_build_x86_windows() { cross_build_x86_windows() {
check_cross check_cross
echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}" echo -e "${YELLOW}cross build --target x86_64-pc-windows-gnu --release${RESET}"
cross build --target x86_64-pc-windows-gnu --release cross build --target x86_64-pc-windows-gnu --release
@@ -217,15 +218,15 @@ cargo_build_x86_windows() {
# Build all releases that GitHub workflow would # Build all releases that GitHub workflow would
# This will download GB's of docker images # This will download GB's of docker images
cargo_build_all() { cross_build_all() {
cargo clean cargo clean
cargo_build_armv6_linux cross_build_armv6_linux
ask_continue ask_continue
cargo_build_aarch64_linux cross_build_aarch64_linux
ask_continue ask_continue
cargo_build_x86_linux cross_build_x86_linux
ask_continue ask_continue
cargo_build_x86_windows cross_build_x86_windows
ask_continue ask_continue
} }
@@ -264,7 +265,7 @@ release_flow() {
get_git_remote_url get_git_remote_url
cargo_test cargo_test
cargo_build_all cross_build_all
cargo_publish cargo_publish
cd "${CWD}" || error_close "Can't find ${CWD}" cd "${CWD}" || error_close "Can't find ${CWD}"
@@ -347,23 +348,23 @@ build_choice() {
exit exit
;; ;;
1) 1)
cargo_build_x86_linux cross_build_x86_linux
exit exit
;; ;;
2) 2)
cargo_build_aarch64_linux cross_build_aarch64_linux
exit exit
;; ;;
3) 3)
cargo_build_armv6_linux cross_build_armv6_linux
exit exit
;; ;;
4) 4)
cargo_build_x86_windows cross_build_x86_windows
exit exit
;; ;;
5) 5)
cargo_build_all cross_build_all
exit exit
;; ;;
esac esac
+17
View File
@@ -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
@@ -4,7 +4,7 @@ networks:
name: oxker-examaple-net name: oxker-examaple-net
services: services:
postgres: postgres:
image: postgres:alpine3.19 image: postgres:alpine3.20
container_name: postgres container_name: postgres
environment: environment:
- POSTGRES_PASSWORD=never_use_this_password_in_production - POSTGRES_PASSWORD=never_use_this_password_in_production
@@ -18,7 +18,7 @@ services:
limits: limits:
memory: 1024M memory: 1024M
redis: redis:
image: redis:alpine3.19 image: redis:alpine3.20
container_name: redis container_name: redis
ipc: private ipc: private
restart: always restart: always
@@ -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
+1 -1
View File
@@ -9,7 +9,7 @@ esac
if [ -n "$SUFFIX" ]; then if [ -n "$SUFFIX" ]; then
OXKER_GZ="oxker_linux_${SUFFIX}.tar.gz" OXKER_GZ="oxker_linux_${SUFFIX}.tar.gz"
wget "https://github.com/mrjackwills/oxker/releases/latest/download/${OXKER_GZ}" curl -L -O "https://github.com/mrjackwills/oxker/releases/latest/download/${OXKER_GZ}"
tar xzvf "${OXKER_GZ}" oxker tar xzvf "${OXKER_GZ}" oxker
install -Dm 755 oxker -t "${HOME}/.local/bin" install -Dm 755 oxker -t "${HOME}/.local/bin"
rm "${OXKER_GZ}" oxker rm "${OXKER_GZ}" oxker
+140 -39
View File
@@ -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) => {
@@ -67,7 +72,7 @@ macro_rules! unit_struct {
} }
} }
impl$name { impl $name {
pub fn get(&self) -> &str { pub fn get(&self) -> &str {
self.0.as_str() self.0.as_str()
} }
@@ -77,20 +82,18 @@ macro_rules! unit_struct {
} }
} }
impl Contains for $name {
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
)
} }
} }
} }
@@ -207,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)))
} }
} }
@@ -274,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}")
@@ -310,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],
} }
} }
@@ -539,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,
} }
@@ -568,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();
@@ -665,14 +715,14 @@ impl Columns {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
name: (Header::Name, 4), name: (Header::Name, 4),
state: (Header::State, 11), state: (Header::State, 5),
status: (Header::Status, 16), status: (Header::Status, 6),
cpu: (Header::Cpu, 7), cpu: (Header::Cpu, 3),
mem: (Header::Memory, 7, 7), mem: (Header::Memory, 7, 7),
id: (Header::Id, 8), id: (Header::Id, 8),
image: (Header::Image, 5), image: (Header::Image, 5),
net_rx: (Header::Rx, 7), net_rx: (Header::Rx, 4),
net_tx: (Header::Tx, 7), net_tx: (Header::Tx, 4),
} }
} }
} }
@@ -682,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
@@ -770,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);
}
} }
+468 -66
View File
@@ -3,6 +3,7 @@ use core::fmt;
use parking_lot::Mutex; use parking_lot::Mutex;
use ratatui::widgets::{ListItem, ListState}; use ratatui::widgets::{ListItem, ListState};
use std::{ use std::{
hash::Hash,
sync::Arc, sync::Arc,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
@@ -54,12 +55,73 @@ impl fmt::Display for Header {
} }
} }
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum FilterBy {
#[default]
Name,
Image,
Status,
All,
}
/// Convert errors into strings to display
impl fmt::Display for FilterBy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Name => "Name",
Self::Image => "Image",
Self::Status => "Status",
Self::All => "All",
}
)
}
}
impl FilterBy {
const fn next(self) -> Option<Self> {
match self {
Self::Name => Some(Self::Image),
Self::Image => Some(Self::Status),
Self::Status => Some(Self::All),
Self::All => None,
}
}
const fn prev(self) -> Option<Self> {
match self {
Self::Name => None,
Self::Image => Some(Self::Name),
Self::Status => Some(Self::Image),
Self::All => Some(Self::Status),
}
}
}
#[derive(Debug, Clone)]
pub struct Filter {
pub term: Option<String>,
pub by: FilterBy,
}
impl Filter {
pub fn new() -> Self {
Self {
term: None,
by: FilterBy::default(),
}
}
}
/// Global app_state, stored in an Arc<Mutex> /// Global app_state, stored in an Arc<Mutex>
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg(not(test))] #[cfg(not(test))]
pub struct AppData { pub struct AppData {
containers: StatefulList<ContainerItem>, containers: StatefulList<ContainerItem>,
error: Option<AppError>, error: Option<AppError>,
filter: Filter,
hidden_containers: Vec<ContainerItem>,
sorted_by: Option<(Header, SortedOrder)>, sorted_by: Option<(Header, SortedOrder)>,
pub args: CliArgs, pub args: CliArgs,
} }
@@ -67,10 +129,12 @@ pub struct AppData {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg(test)] #[cfg(test)]
pub struct AppData { pub struct AppData {
pub args: CliArgs,
pub containers: StatefulList<ContainerItem>, pub containers: StatefulList<ContainerItem>,
pub error: Option<AppError>, pub error: Option<AppError>,
pub filter: Filter,
pub hidden_containers: Vec<ContainerItem>,
pub sorted_by: Option<(Header, SortedOrder)>, pub sorted_by: Option<(Header, SortedOrder)>,
pub args: CliArgs,
} }
impl AppData { impl AppData {
@@ -79,8 +143,10 @@ impl AppData {
Self { Self {
args, args,
containers: StatefulList::new(vec![]), containers: StatefulList::new(vec![]),
hidden_containers: vec![],
error: None, error: None,
sorted_by: None, sorted_by: None,
filter: Filter::new(),
} }
} }
@@ -93,6 +159,126 @@ impl AppData {
.as_secs() .as_secs()
} }
/// Filter related methods
/// Get the current filter term
pub const fn get_filter_term(&self) -> Option<&String> {
self.filter.term.as_ref()
}
/// Get the current filter by choice
pub const fn get_filter_by(&self) -> FilterBy {
self.filter.by
}
/// Check if a given container can be inserted into the "visible" list, based on current filter term and filter_by
fn can_insert(&self, container: &ContainerItem) -> bool {
self.filter.term.as_ref().map_or(true, |term| {
let term = term.to_lowercase();
match self.filter.by {
FilterBy::All => {
container.name.contains(&term)
|| container.image.contains(&term)
|| container.status.contains(&term)
}
FilterBy::Image => container.image.contains(&term),
FilterBy::Name => container.name.contains(&term),
FilterBy::Status => container.status.contains(&term),
}
})
}
/// Remove items from the containers list based on the filter term, and insert into a "hidden" vec
/// sets the state to start if any filtering has occurred
/// Also search in the "hidden" vec for items and insert back into the main containers vec
fn filter_containers(&mut self) {
let pre_len = self.get_container_len();
if !self.hidden_containers.is_empty() {
let (mut new_items, tmp_items): (Vec<_>, Vec<_>) = self
.hidden_containers
.iter()
.cloned()
.partition(|item| self.can_insert(item));
while let Some(x) = new_items.pop() {
self.containers.items.push(x);
}
self.hidden_containers = tmp_items;
}
let (new_items, tmp_items) = self
.containers
.items
.iter()
.cloned()
.partition(|item| self.can_insert(item));
self.containers.items = new_items;
self.hidden_containers.extend(tmp_items);
self.sort_containers();
if self.get_container_len() != pre_len {
self.containers.start();
}
}
/// Re-filter the containers, used after the filter.by has been changed
fn re_filter(&mut self) {
self.containers.items.append(&mut self.hidden_containers);
self.hidden_containers = vec![];
self.filter_containers();
}
/// Set a single char into the filter term
pub fn filter_term_push(&mut self, c: char) {
if let Some(term) = self.filter.term.as_mut() {
term.push(c);
} else {
self.filter.term = Some(format!("{c}"));
};
self.filter_containers();
}
/// Delete the final char of the filter term
pub fn filter_term_pop(&mut self) {
if let Some(term) = self.filter.term.as_mut() {
// should now search for items in the tmp vec, and insert into containers if found
term.pop();
if term.is_empty() {
self.filter.term = None;
}
}
self.filter_containers();
}
// change the filter_by option
pub fn filter_by_next(&mut self) {
if let Some(by) = self.filter.by.next() {
self.filter.by = by;
self.re_filter();
}
}
// change the filter_by option
pub fn filter_by_prev(&mut self) {
if let Some(by) = self.filter.by.prev() {
self.filter.by = by;
self.re_filter();
}
}
/// Remove the filter completely
pub fn filter_term_clear(&mut self) {
self.filter.term = None;
while let Some(i) = self.hidden_containers.pop() {
if self.get_container_by_id(&i.id).is_none() {
self.containers.items.push(i);
};
}
self.sort_containers();
}
/// Container sort related methods /// Container sort related methods
/// Change the sorted order, also set the selected container state to match new order /// Change the sorted order, also set the selected container state to match new order
@@ -149,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
@@ -206,7 +393,7 @@ impl AppData {
/// Container state methods /// Container state methods
/// Just get the total number of containers /// Get the total number of none "hidden" containers
pub fn get_container_len(&self) -> usize { pub fn get_container_len(&self) -> usize {
self.containers.items.len() self.containers.items.len()
} }
@@ -216,9 +403,14 @@ impl AppData {
&self.containers.items &self.containers.items
} }
/// Get title for containers section /// Get title for containers section, add a suffix indicating if the containers are currently under filter
pub fn container_title(&self) -> String { pub fn container_title(&self) -> String {
self.containers.get_state_title() let suffix = if !self.hidden_containers.is_empty() && !self.containers.items.is_empty() {
" - filtered"
} else {
""
};
format!("{}{}", self.containers.get_state_title(), suffix)
} }
/// Select the first container /// Select the first container
@@ -260,8 +452,8 @@ impl AppData {
let mut longest_private = 10; let mut longest_private = 10;
let mut longest_public = 9; let mut longest_public = 9;
for item in &self.containers.items { for item in [&self.containers.items, &self.hidden_containers] {
// if let Some(ports) = item.ports.as_ref() { for item in item {
longest_ip = longest_ip.max( longest_ip = longest_ip.max(
item.ports item.ports
.iter() .iter()
@@ -284,11 +476,11 @@ impl AppData {
.unwrap_or(6), .unwrap_or(6),
); );
} }
// } }
(longest_ip, longest_private, longest_public) (longest_ip, longest_private, longest_public)
// )
} }
/// Get Option of the current selected container's ports, sorted by private port /// Get Option of the current selected container's ports, sorted by private port
pub fn get_selected_ports(&mut self) -> Option<(Vec<ContainerPorts>, State)> { pub fn get_selected_ports(&mut self) -> Option<(Vec<ContainerPorts>, State)> {
if let Some(item) = self.get_mut_selected_container() { if let Some(item) = self.get_mut_selected_container() {
@@ -307,11 +499,16 @@ impl AppData {
.and_then(|i| self.containers.items.get_mut(i)) .and_then(|i| self.containers.items.get_mut(i))
} }
/// return a mutable container by given id /// Get a mutable container by given id
fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
self.containers.items.iter_mut().find(|i| &i.id == id) self.containers.items.iter_mut().find(|i| &i.id == id)
} }
/// Get a mutable container by given id in the tmp_container vec
fn get_hidden_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
self.hidden_containers.iter_mut().find(|i| &i.id == id)
}
/// Get the ContainerName of by ID /// Get the ContainerName of by ID
pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<ContainerName> { pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<ContainerName> {
self.containers self.containers
@@ -333,6 +530,7 @@ impl AppData {
self.get_selected_container() self.get_selected_container()
.map(|i| (i.id.clone(), i.state, i.name.get().to_owned())) .map(|i| (i.id.clone(), i.state, i.name.get().to_owned()))
} }
/// Selected DockerCommand methods /// Selected DockerCommand methods
/// Get the current selected docker command /// Get the current selected docker command
@@ -392,8 +590,8 @@ impl AppData {
/// Logs related methods /// Logs related methods
/// Get the title for log panel for selected container, will be either /// Get the title for log panel for selected container, will be either
/// 1) "logs x/x - container_name" where container_name is 32 chars max /// 1) "logs x/x - container_name - container_image"
/// 2) "logs - container_name" when no logs found, again 32 chars max /// 2) "logs - container_name - container_image" when no logs found
/// 3) "" no container currently selected - aka no containers on system /// 3) "" no container currently selected - aka no containers on system
pub fn get_log_title(&self) -> String { pub fn get_log_title(&self) -> String {
self.get_selected_container() self.get_selected_container()
@@ -404,7 +602,7 @@ impl AppData {
} else { } else {
format!("{logs_len} ") format!("{logs_len} ")
}; };
format!("{}- {}", prefix, ci.name.get()) format!("{}- {} - {}", prefix, ci.name.get(), ci.image.get())
}) })
} }
@@ -467,17 +665,17 @@ impl AppData {
/// Error related methods /// Error related methods
/// return single app_state error /// Get single app_state error
pub const fn get_error(&self) -> Option<AppError> { pub const fn get_error(&self) -> Option<AppError> {
self.error self.error
} }
/// remove single app_state error /// Remove single app_state error
pub fn remove_error(&mut self) { pub fn remove_error(&mut self) {
self.error = None; self.error = None;
} }
/// insert single app_state error /// Insert single app_state error
pub fn set_error(&mut self, error: AppError, gui_state: &Arc<Mutex<GuiState>>, status: Status) { pub fn set_error(&mut self, error: AppError, gui_state: &Arc<Mutex<GuiState>>, status: Status) {
gui_state.lock().status_push(status); gui_state.lock().status_push(status);
self.error = Some(error); self.error = Some(error);
@@ -498,12 +696,14 @@ impl AppData {
/// Find the widths for the strings in the containers panel. /// Find the widths for the strings in the containers panel.
/// So can display nicely and evenly /// So can display nicely and evenly
/// Searches in both contains & hidden_containers
pub fn get_width(&self) -> Columns { pub fn get_width(&self) -> Columns {
let mut columns = Columns::new(); let mut columns = Columns::new();
let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12); let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12);
// Should probably find a refactor here somewhere // Should probably find a refactor here somewhere
for container in &self.containers.items { for container in [&self.containers.items, &self.hidden_containers] {
for container in container {
let cpu_count = count( let cpu_count = count(
&container &container
.cpu_stats .cpu_stats
@@ -520,7 +720,6 @@ impl AppData {
.to_string(), .to_string(),
); );
// Issue here!
columns.cpu.1 = columns.cpu.1.max(cpu_count); columns.cpu.1 = columns.cpu.1.max(cpu_count);
columns.image.1 = columns.image.1.max(count(&container.image.to_string())); columns.image.1 = columns.image.1.max(count(&container.image.to_string()));
columns.mem.1 = columns.mem.1.max(mem_current_count); columns.mem.1 = columns.mem.1.max(mem_current_count);
@@ -529,13 +728,23 @@ 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
} }
/// Update related methods /// Update related methods
/// Get mutable reference to a container in the containers vec & the hidden_containers vec
fn get_any_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
if self.get_hidden_container_by_id(id).is_some() {
self.get_hidden_container_by_id(id)
} else {
self.get_container_by_id(id)
}
}
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once /// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
/// Will also, if a sort is set, sort the containers /// Will also, if a sort is set, sort the containers
pub fn update_stats_by_id( pub fn update_stats_by_id(
@@ -547,7 +756,7 @@ impl AppData {
rx: u64, rx: u64,
tx: u64, tx: u64,
) { ) {
if let Some(container) = self.get_container_by_id(id) { if let Some(container) = self.get_any_container_by_id(id) {
if container.cpu_stats.len() >= 60 { if container.cpu_stats.len() >= 60 {
container.cpu_stats.pop_front(); container.cpu_stats.pop_front();
} }
@@ -628,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()
@@ -642,8 +852,8 @@ impl AppData {
let created = i let created = i
.created .created
.map_or(0, |i| u64::try_from(i).unwrap_or_default()); .map_or(0, |i| u64::try_from(i).unwrap_or_default());
// If container info already in containers Vec, then just update details
if let Some(item) = self.get_container_by_id(&id) { if let Some(item) = self.get_any_container_by_id(&id) {
if item.name.get() != name { if item.name.get() != name {
item.name.set(name); item.name.set(name);
}; };
@@ -668,24 +878,29 @@ impl AppData {
item.image.set(image); item.image.set(image);
}; };
} else { } else {
// container not known, so make new ContainerItem and push into containers Vec // container not known, so make new ContainerItem and push into containers Ve
let container = ContainerItem::new( let container = ContainerItem::new(
created, id, image, is_oxker, name, ports, state, status, created, id, image, is_oxker, name, ports, state, status,
); );
let can_insert = self.can_insert(&container);
if can_insert {
self.containers.items.push(container); self.containers.items.push(container);
} else {
self.hidden_containers.push(container);
}
} }
} }
} }
} }
/// update logs of a given container, based on id /// Update logs of a given container, based on id
pub fn update_log_by_id(&mut self, logs: Vec<String>, id: &ContainerId) { pub fn update_log_by_id(&mut self, logs: Vec<String>, id: &ContainerId) {
let color = self.args.color; let color = self.args.color;
let raw = self.args.raw; let raw = self.args.raw;
let timestamp = self.args.timestamp; let timestamp = self.args.timestamp;
if let Some(container) = self.get_container_by_id(id) { if let Some(container) = self.get_any_container_by_id(id) {
if !container.is_oxker { if !container.is_oxker {
container.last_updated = Self::get_systemtime(); container.last_updated = Self::get_systemtime();
let current_len = container.logs.len(); let current_len = container.logs.len();
@@ -770,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;
@@ -804,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
@@ -1129,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()
)) ))
); );
@@ -1143,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()
)) ))
); );
@@ -1171,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()
)) ))
); );
@@ -1196,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()
)) ))
); );
@@ -1210,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()
)) ))
); );
@@ -1291,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()
)) ))
); );
@@ -1381,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)]);
@@ -1422,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,
@@ -1433,6 +1649,163 @@ mod tests {
test_state(State::Unknown, &mut vec![DockerControls::Delete]); test_state(State::Unknown, &mut vec![DockerControls::Delete]);
} }
// ****** //
// Filter //
// ****** //
#[test]
/// Data is filtered correctly by name
fn test_app_data_filter_by_name() {
let (_, containers) = gen_containers();
let mut app_data = gen_appdata(&containers);
assert!(app_data.get_filter_term().is_none());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('_');
app_data.filter_term_push('2');
assert_eq!(app_data.get_filter_term(), Some(&"_2".to_string()));
app_data.filter_containers();
let post_len = app_data.containers.items.len();
assert!(pre_len != post_len);
assert_eq!(post_len, 1);
// Can insert checks against the current filter term
assert!(app_data.can_insert(&containers[1]));
assert!(!app_data.can_insert(&containers[0]));
assert!(!app_data.can_insert(&containers[2]));
}
#[test]
/// Data is filtered correctly by image
fn test_app_data_filter_by_image() {
let (_, containers) = gen_containers();
let mut app_data = gen_appdata(&containers);
assert!(app_data.get_filter_term().is_none());
let pre_len = app_data.containers.items.len();
for c in ['i', 'm', 'a', 'g', 'e', '_', '2'] {
app_data.filter_term_push(c);
}
// app_data.filter_term_push('2');
app_data.filter_by_next();
assert_eq!(app_data.get_filter_by(), FilterBy::Image);
assert_eq!(app_data.get_filter_term(), Some(&"image_2".to_string()));
app_data.filter_containers();
let post_len = app_data.containers.items.len();
assert!(pre_len != post_len);
assert_eq!(post_len, 1);
assert!(!app_data.can_insert(&containers[0]));
assert!(app_data.can_insert(&containers[1]));
assert!(!app_data.can_insert(&containers[2]));
}
#[test]
/// Data is filtered correctly by status
fn test_app_data_filter_by_status() {
let (_, mut containers) = gen_containers();
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());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('x');
app_data.filter_by_next();
app_data.filter_by_next();
assert_eq!(app_data.get_filter_by(), FilterBy::Status);
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
app_data.filter_containers();
let post_len = app_data.containers.items.len();
assert!(pre_len != post_len);
assert_eq!(post_len, 1);
assert!(app_data.can_insert(&containers[0]));
assert!(!app_data.can_insert(&containers[1]));
assert!(!app_data.can_insert(&containers[2]));
}
#[test]
/// Data is filtered correctly by all
fn test_app_data_filter_by_all() {
let (_, mut containers) = gen_containers();
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());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('x');
app_data.filter_by_next();
app_data.filter_by_next();
app_data.filter_by_next();
assert_eq!(app_data.get_filter_by(), FilterBy::All);
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
app_data.filter_containers();
let post_len = app_data.containers.items.len();
assert!(pre_len != post_len);
assert_eq!(post_len, 1);
assert!(app_data.can_insert(&containers[0]));
assert!(!app_data.can_insert(&containers[1]));
assert!(!app_data.can_insert(&containers[2]));
}
#[test]
/// Data is filtered correctly after various next() and previous() commands
fn test_app_data_filter_prev() {
let (_, mut containers) = gen_containers();
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());
let pre_len = app_data.containers.items.len();
app_data.filter_term_push('x');
app_data.filter_by_next();
app_data.filter_by_next();
assert_eq!(app_data.get_filter_by(), FilterBy::Status);
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
app_data.filter_containers();
let post_len = app_data.containers.items.len();
assert!(pre_len != post_len);
assert_eq!(post_len, 1);
assert!(app_data.can_insert(&containers[0]));
assert!(!app_data.can_insert(&containers[1]));
assert!(!app_data.can_insert(&containers[2]));
app_data.filter_by_prev();
assert_eq!(app_data.get_filter_by(), FilterBy::Image);
assert_eq!(app_data.get_filter_term(), Some(&"x".to_string()));
app_data.filter_containers();
let post_len = app_data.containers.items.len();
assert!(pre_len != post_len);
assert_eq!(post_len, 0);
assert!(!app_data.can_insert(&containers[0]));
assert!(!app_data.can_insert(&containers[1]));
assert!(!app_data.can_insert(&containers[2]));
}
// **** // // **** //
// Logs // // Logs //
// **** // // **** //
@@ -1451,18 +1824,18 @@ mod tests {
// No logs // No logs
app_data.containers.start(); app_data.containers.start();
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " - container_1"); assert_eq!(result, " - container_1 - image_1");
// On last line of logs // On last line of logs
let logs = (1..=3).map(|i| format!("{i}")).collect::<Vec<_>>(); let logs = (1..=3).map(|i| format!("{i}")).collect::<Vec<_>>();
app_data.update_log_by_id(logs, &ids[0]); app_data.update_log_by_id(logs, &ids[0]);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1"); assert_eq!(result, " 3/3 - container_1 - image_1");
// Change log state to no longer be at the end // Change log state to no longer be at the end
app_data.log_previous(); app_data.log_previous();
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 2/3 - container_1"); assert_eq!(result, " 2/3 - container_1 - image_1");
} }
#[test] #[test]
@@ -1478,23 +1851,23 @@ mod tests {
app_data.containers_start(); app_data.containers_start();
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " - container_1"); assert_eq!(result, " - container_1 - image_1");
// change container // change container
app_data.containers_next(); app_data.containers_next();
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " - container_2"); assert_eq!(result, " - container_2 - image_2");
// On last line of logs // On last line of logs
let logs = (1..=3).map(|i| format!("{i}")).collect::<Vec<_>>(); let logs = (1..=3).map(|i| format!("{i}")).collect::<Vec<_>>();
app_data.update_log_by_id(logs, &ids[1]); app_data.update_log_by_id(logs, &ids[1]);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_2"); assert_eq!(result, " 3/3 - container_2 - image_2");
// Change log state to no longer be at the end // Change log state to no longer be at the end
app_data.log_previous(); app_data.log_previous();
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 2/3 - container_2"); assert_eq!(result, " 2/3 - container_2 - image_2");
} }
#[test] #[test]
@@ -1522,7 +1895,7 @@ mod tests {
assert_eq!(result.len(), 3); assert_eq!(result.len(), 3);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1"); assert_eq!(result, " 3/3 - container_1 - image_1");
} }
#[test] #[test]
@@ -1542,7 +1915,7 @@ mod tests {
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 1/3 - container_1"); assert_eq!(result, " 1/3 - container_1 - image_1");
} }
#[test] #[test]
@@ -1562,7 +1935,7 @@ mod tests {
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 1/3 - container_1"); assert_eq!(result, " 1/3 - container_1 - image_1");
app_data.log_end(); app_data.log_end();
let result = app_data.get_log_state(); let result = app_data.get_log_state();
@@ -1571,7 +1944,7 @@ mod tests {
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1"); assert_eq!(result, " 3/3 - container_1 - image_1");
} }
#[test] #[test]
@@ -1592,7 +1965,7 @@ mod tests {
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 1/3 - container_1"); assert_eq!(result, " 1/3 - container_1 - image_1");
app_data.log_next(); app_data.log_next();
@@ -1602,7 +1975,7 @@ mod tests {
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 2/3 - container_1"); assert_eq!(result, " 2/3 - container_1 - image_1");
app_data.log_next(); app_data.log_next();
let result = app_data.get_log_state(); let result = app_data.get_log_state();
@@ -1611,7 +1984,7 @@ mod tests {
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1"); assert_eq!(result, " 3/3 - container_1 - image_1");
app_data.log_next(); app_data.log_next();
let result = app_data.get_log_state(); let result = app_data.get_log_state();
@@ -1620,7 +1993,7 @@ mod tests {
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1"); assert_eq!(result, " 3/3 - container_1 - image_1");
} }
#[test] #[test]
@@ -1641,7 +2014,7 @@ mod tests {
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 3/3 - container_1"); assert_eq!(result, " 3/3 - container_1 - image_1");
app_data.log_previous(); app_data.log_previous();
@@ -1650,7 +2023,7 @@ mod tests {
assert_eq!(result.as_ref().unwrap().selected(), Some(1)); assert_eq!(result.as_ref().unwrap().selected(), Some(1));
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 2/3 - container_1"); assert_eq!(result, " 2/3 - container_1 - image_1");
app_data.log_previous(); app_data.log_previous();
let result = app_data.get_log_state(); let result = app_data.get_log_state();
@@ -1658,7 +2031,7 @@ mod tests {
assert_eq!(result.as_ref().unwrap().selected(), Some(0)); assert_eq!(result.as_ref().unwrap().selected(), Some(0));
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 1/3 - container_1"); assert_eq!(result, " 1/3 - container_1 - image_1");
app_data.log_previous(); app_data.log_previous();
let result = app_data.get_log_state(); let result = app_data.get_log_state();
@@ -1666,7 +2039,7 @@ mod tests {
assert_eq!(result.as_ref().unwrap().selected(), Some(0)); assert_eq!(result.as_ref().unwrap().selected(), Some(0));
assert_eq!(result.unwrap().offset(), 0); assert_eq!(result.unwrap().offset(), 0);
let result = app_data.get_log_title(); let result = app_data.get_log_title();
assert_eq!(result, " 1/3 - container_1"); assert_eq!(result, " 1/3 - container_1 - image_1");
} }
// ********** // // ********** //
@@ -1696,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),
) )
)) ))
); );
@@ -1720,9 +2093,9 @@ mod tests {
let result = app_data.get_width(); let result = app_data.get_width();
let expected = Columns { let expected = Columns {
name: (Header::Name, 11), name: (Header::Name, 11),
state: (Header::State, 11), state: (Header::State, 9),
status: (Header::Status, 16), status: (Header::Status, 9),
cpu: (Header::Cpu, 7), cpu: (Header::Cpu, 6),
mem: (Header::Memory, 7, 7), mem: (Header::Memory, 7, 7),
id: (Header::Id, 8), id: (Header::Id, 8),
image: (Header::Image, 7), image: (Header::Image, 7),
@@ -1732,6 +2105,32 @@ mod tests {
assert_eq!(result, expected); assert_eq!(result, expected);
} }
#[test]
/// Header widths return correctly when some containers hidden
fn test_app_data_get_width_filtered() {
let (_ids, mut containers) = gen_containers();
containers[0].name = ContainerName::from("some_longer_name_with_filter");
let mut app_data = gen_appdata(&containers);
let result = app_data.get_width();
let expected = Columns {
name: (Header::Name, 28),
state: (Header::State, 9),
status: (Header::Status, 9),
cpu: (Header::Cpu, 6),
mem: (Header::Memory, 7, 7),
id: (Header::Id, 8),
image: (Header::Image, 7),
net_rx: (Header::Rx, 7),
net_tx: (Header::Tx, 7),
};
assert_eq!(result, expected);
app_data.filter_term_push('c');
app_data.filter_containers();
assert_eq!(result, expected);
}
// ***** // // ***** //
// Ports // // Ports //
// ***** // // ***** //
@@ -1791,7 +2190,7 @@ mod tests {
public: None public: None
} }
], ],
State::Running State::Running(RunningState::Healthy),
)) ))
); );
@@ -1800,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)))
);
} }
// ************** // // ************** //
+27 -20
View File
@@ -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},
@@ -201,7 +201,7 @@ impl DockerData {
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here /// Get all current containers, handle into ContainerItem in the app_data struct rather than here
/// Just make sure that items sent are guaranteed to have an id /// Just make sure that items sent are guaranteed to have an id
/// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set /// If in a containerised runtime, will ignore any container that uses the `/app/oxker` as an entry point, unless the `-s` flag is set
pub async fn update_all_containers(&mut self) -> Vec<(State, ContainerId)> { pub async fn update_all_containers(&self) -> Vec<(State, ContainerId)> {
let containers = self let containers = self
.docker .docker
.list_containers(Some(ListContainersOptions::<String> { .list_containers(Some(ListContainersOptions::<String> {
@@ -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<_>>()
} }
@@ -271,7 +279,7 @@ impl DockerData {
} }
/// Update all logs, spawn each container into own tokio::spawn thread /// Update all logs, spawn each container into own tokio::spawn thread
fn init_all_logs(&mut self, all_ids: &[(State, ContainerId)]) { fn init_all_logs(&self, all_ids: &[(State, ContainerId)]) {
for (_, id) in all_ids { for (_, id) in all_ids {
let docker = Arc::clone(&self.docker); let docker = Arc::clone(&self.docker);
let app_data = Arc::clone(&self.app_data); let app_data = Arc::clone(&self.app_data);
@@ -303,13 +311,14 @@ impl DockerData {
}; };
self.update_all_container_stats(&all_ids); self.update_all_container_stats(&all_ids);
self.app_data.lock().sort_containers(); self.app_data.lock().sort_containers();
self.gui_state.lock().stop_loading_animation(Uuid::nil());
} }
/// Initialize docker container data, before any messages are received /// Initialize docker container data, before any messages are received
async fn initialise_container_data(&mut self) { async fn initialise_container_data(&mut self) {
self.gui_state.lock().status_push(Status::Init); self.gui_state.lock().status_push(Status::Init);
let loading_uuid = Uuid::new_v4(); let loading_uuid = Uuid::new_v4();
let loading_handle = GuiState::start_loading_animation(&self.gui_state, loading_uuid); GuiState::start_loading_animation(&self.gui_state, loading_uuid);
let all_ids = self.update_all_containers().await; let all_ids = self.update_all_containers().await;
self.update_all_container_stats(&all_ids); self.update_all_container_stats(&all_ids);
@@ -323,9 +332,7 @@ impl DockerData {
self.init = None; self.init = None;
} }
} }
self.gui_state self.gui_state.lock().stop_loading_animation(loading_uuid);
.lock()
.stop_loading_animation(&loading_handle, loading_uuid);
self.gui_state.lock().status_del(Status::Init); self.gui_state.lock().status_del(Status::Init);
} }
@@ -356,27 +363,27 @@ impl DockerData {
} }
DockerMessage::Pause(id) => { DockerMessage::Pause(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid); GuiState::start_loading_animation(&gui_state, uuid);
if docker.pause_container(id.get()).await.is_err() { if docker.pause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Pause, &gui_state); Self::set_error(&app_data, DockerControls::Pause, &gui_state);
} }
gui_state.lock().stop_loading_animation(&handle, uuid); gui_state.lock().stop_loading_animation(uuid);
}); });
self.update_everything().await; self.update_everything().await;
} }
DockerMessage::Restart(id) => { DockerMessage::Restart(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid); GuiState::start_loading_animation(&gui_state, uuid);
if docker.restart_container(id.get(), None).await.is_err() { if docker.restart_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Restart, &gui_state); Self::set_error(&app_data, DockerControls::Restart, &gui_state);
} }
gui_state.lock().stop_loading_animation(&handle, uuid); gui_state.lock().stop_loading_animation(uuid);
}); });
self.update_everything().await; self.update_everything().await;
} }
DockerMessage::Start(id) => { DockerMessage::Start(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid); GuiState::start_loading_animation(&gui_state, uuid);
if docker if docker
.start_container(id.get(), None::<StartContainerOptions<String>>) .start_container(id.get(), None::<StartContainerOptions<String>>)
.await .await
@@ -384,33 +391,33 @@ impl DockerData {
{ {
Self::set_error(&app_data, DockerControls::Start, &gui_state); Self::set_error(&app_data, DockerControls::Start, &gui_state);
} }
gui_state.lock().stop_loading_animation(&handle, uuid); gui_state.lock().stop_loading_animation(uuid);
}); });
self.update_everything().await; self.update_everything().await;
} }
DockerMessage::Stop(id) => { DockerMessage::Stop(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid); GuiState::start_loading_animation(&gui_state, uuid);
if docker.stop_container(id.get(), None).await.is_err() { if docker.stop_container(id.get(), None).await.is_err() {
Self::set_error(&app_data, DockerControls::Stop, &gui_state); Self::set_error(&app_data, DockerControls::Stop, &gui_state);
} }
gui_state.lock().stop_loading_animation(&handle, uuid); gui_state.lock().stop_loading_animation(uuid);
}); });
self.update_everything().await; self.update_everything().await;
} }
DockerMessage::Resume(id) => { DockerMessage::Resume(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid); GuiState::start_loading_animation(&gui_state, uuid);
if docker.unpause_container(id.get()).await.is_err() { if docker.unpause_container(id.get()).await.is_err() {
Self::set_error(&app_data, DockerControls::Resume, &gui_state); Self::set_error(&app_data, DockerControls::Resume, &gui_state);
} }
gui_state.lock().stop_loading_animation(&handle, uuid); gui_state.lock().stop_loading_animation(uuid);
}); });
self.update_everything().await; self.update_everything().await;
} }
DockerMessage::Delete(id) => { DockerMessage::Delete(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid); GuiState::start_loading_animation(&gui_state, uuid);
if docker if docker
.remove_container( .remove_container(
id.get(), id.get(),
@@ -425,7 +432,7 @@ impl DockerData {
{ {
Self::set_error(&app_data, DockerControls::Stop, &gui_state); Self::set_error(&app_data, DockerControls::Stop, &gui_state);
} }
gui_state.lock().stop_loading_animation(&handle, uuid); gui_state.lock().stop_loading_animation(uuid);
}); });
self.update_everything().await; self.update_everything().await;
self.gui_state.lock().set_delete_container(None); self.gui_state.lock().set_delete_container(None);
+7 -2
View File
@@ -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(
+99 -46
View File
@@ -71,6 +71,7 @@ impl InputHandler {
Status::Error, Status::Error,
Status::Help, Status::Help,
Status::DeleteConfirm, Status::DeleteConfirm,
Status::Filter,
]) { ]) {
self.mouse_press(mouse_event); self.mouse_press(mouse_event);
} }
@@ -125,7 +126,7 @@ impl InputHandler {
let is_oxker = self.app_data.lock().is_oxker(); let is_oxker = self.app_data.lock().is_oxker();
if !is_oxker && tty_readable() { if !is_oxker && tty_readable() {
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let handle = GuiState::start_loading_animation(&self.gui_state, uuid); GuiState::start_loading_animation(&self.gui_state, uuid);
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>(); let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok(); self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
@@ -143,7 +144,7 @@ impl InputHandler {
}, },
); );
} }
self.gui_state.lock().stop_loading_animation(&handle, uuid); self.gui_state.lock().stop_loading_animation(uuid);
} }
} }
@@ -177,7 +178,7 @@ impl InputHandler {
} }
/// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file /// Save the currently selected containers logs into a `[container_name]_[timestamp].log` file
async fn s_key(&mut self) { async fn s_key(&self) {
/// This is the inner workings, *inlined* here to return a Result /// This is the inner workings, *inlined* here to return a Result
async fn save_logs( async fn save_logs(
app_data: &Arc<Mutex<AppData>>, app_data: &Arc<Mutex<AppData>>,
@@ -248,7 +249,7 @@ impl InputHandler {
self.gui_state.lock().status_push(log_status); self.gui_state.lock().status_push(log_status);
let uuid = Uuid::new_v4(); let uuid = Uuid::new_v4();
let handle = GuiState::start_loading_animation(&self.gui_state, uuid); GuiState::start_loading_animation(&self.gui_state, uuid);
if save_logs(&self.app_data, &self.gui_state, &self.docker_tx) if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
.await .await
.is_err() .is_err()
@@ -260,12 +261,12 @@ impl InputHandler {
); );
} }
self.gui_state.lock().status_del(log_status); self.gui_state.lock().status_del(log_status);
self.gui_state.lock().stop_loading_animation(&handle, uuid); self.gui_state.lock().stop_loading_animation(uuid);
} }
} }
/// Send docker command, if the Commands panel is selected /// Send docker command, if the Commands panel is selected
async fn enter_key(&mut self) { async fn enter_key(&self) {
// This isn't great, just means you can't send docker commands before full initialization of the program // This isn't great, just means you can't send docker commands before full initialization of the program
let panel = self.gui_state.lock().get_selected_panel(); let panel = self.gui_state.lock().get_selected_panel();
if panel == SelectablePanel::Commands { if panel == SelectablePanel::Commands {
@@ -306,7 +307,7 @@ impl InputHandler {
} }
/// Change the the "next" selectable panel /// Change the the "next" selectable panel
fn tab_key(&mut self) { fn tab_key(&self) {
let is_containers = let is_containers =
self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers; self.gui_state.lock().get_selected_panel() == SelectablePanel::Containers;
let count = if self.app_data.lock().get_container_len() == 0 && is_containers { let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
@@ -320,7 +321,7 @@ impl InputHandler {
} }
/// Change to previously selected panel /// Change to previously selected panel
fn back_tab_key(&mut self) { fn back_tab_key(&self) {
let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs; let is_containers = self.gui_state.lock().get_selected_panel() == SelectablePanel::Logs;
let count = if self.app_data.lock().get_container_len() == 0 && is_containers { let count = if self.app_data.lock().get_container_len() == 0 && is_containers {
2 2
@@ -332,7 +333,7 @@ impl InputHandler {
} }
} }
fn home_key(&mut self) { fn home_key(&self) {
let mut locked_data = self.app_data.lock(); let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel(); let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel { match selected_panel {
@@ -343,7 +344,7 @@ impl InputHandler {
} }
/// Go to end of the list of the currently selected panel /// Go to end of the list of the currently selected panel
fn end_key(&mut self) { fn end_key(&self) {
let mut locked_data = self.app_data.lock(); let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel(); let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel { match selected_panel {
@@ -353,36 +354,8 @@ impl InputHandler {
} }
} }
/// Handle keyboard button events /// Actions to take when in Help status active
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) { fn handle_help(&mut self, key_code: KeyCode) {
let contains_delete = self
.gui_state
.lock()
.status_contains(&[Status::DeleteConfirm]);
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
let contains_error = contains(Status::Error);
let contains_help = contains(Status::Help);
let contains_exec = contains(Status::Exec);
if !contains_exec {
// 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_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() {
self.quit().await;
}
if contains_error {
match key_code {
KeyCode::Esc | KeyCode::Char('c' | 'C') => {
self.app_data.lock().remove_error();
self.gui_state.lock().status_del(Status::Error);
}
_ => (),
}
} else if contains_help {
match key_code { match key_code {
KeyCode::Esc | KeyCode::Char('h' | 'H') => { KeyCode::Esc | KeyCode::Char('h' | 'H') => {
self.gui_state.lock().status_del(Status::Help); self.gui_state.lock().status_del(Status::Help);
@@ -390,13 +363,56 @@ impl InputHandler {
KeyCode::Char('m' | 'M') => self.m_key(), KeyCode::Char('m' | 'M') => self.m_key(),
_ => (), _ => (),
} }
} else if contains_delete { }
/// Actions to take when Error status active
fn handle_error(&self, key_code: KeyCode) {
match key_code {
KeyCode::Esc | KeyCode::Char('c' | 'C') => {
self.app_data.lock().remove_error();
self.gui_state.lock().status_del(Status::Error);
}
_ => (),
}
}
/// Actions to take when Delete status active
async fn handle_delete(&self, key_code: KeyCode) {
match key_code { match key_code {
KeyCode::Char('y' | 'Y') => self.confirm_delete().await, KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(), KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(),
_ => (), _ => (),
} }
} else { }
/// Actions to take when Filter status active
fn handle_filter(&self, key_code: KeyCode) {
match key_code {
KeyCode::Esc => {
self.app_data.lock().filter_term_clear();
self.gui_state.lock().status_del(Status::Filter);
}
KeyCode::Enter | KeyCode::F(1) | KeyCode::Char('/') => {
self.gui_state.lock().status_del(Status::Filter);
}
KeyCode::Backspace => {
self.app_data.lock().filter_term_pop();
}
KeyCode::Char(x) => {
self.app_data.lock().filter_term_push(x);
}
KeyCode::Right => {
self.app_data.lock().filter_by_next();
}
KeyCode::Left => {
self.app_data.lock().filter_by_prev();
}
_ => (),
}
}
/// Handle button presses in all other scenarios
async fn handle_others(&mut self, key_code: KeyCode) {
match key_code { match key_code {
KeyCode::Char('0') => self.app_data.lock().reset_sorted(), KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
KeyCode::Char('1') => self.sort(Header::Name), KeyCode::Char('1') => self.sort(Header::Name),
@@ -422,6 +438,10 @@ impl InputHandler {
self.previous(); self.previous();
} }
} }
KeyCode::F(1) | KeyCode::Char('/') => {
self.gui_state.lock().status_push(Status::Filter);
self.docker_tx.send(DockerMessage::Update).await.ok();
}
KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(), KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
KeyCode::PageDown => { KeyCode::PageDown => {
for _ in 0..=6 { for _ in 0..=6 {
@@ -432,11 +452,44 @@ impl InputHandler {
_ => (), _ => (),
} }
} }
/// Handle keyboard button events
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
let contains_delete = self
.gui_state
.lock()
.status_contains(&[Status::DeleteConfirm]);
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
let contains_error = contains(Status::Error);
let contains_help = contains(Status::Help);
let contains_exec = contains(Status::Exec);
let contains_filter: bool = contains(Status::Filter);
if !contains_exec {
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() && !contains_filter {
// Always just quit on Ctrl + c/C or q/Q, unless in FIlter status active
self.quit().await;
}
if contains_error {
self.handle_error(key_code);
} else if contains_help {
self.handle_help(key_code);
} else if contains_filter {
self.handle_filter(key_code);
} else if contains_delete {
self.handle_delete(key_code).await;
} else {
self.handle_others(key_code).await;
}
} }
} }
/// Check if a button press interacts with either the yes or no buttons in the delete container confirm window /// 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) { async fn button_intersect(&self, mouse_event: MouseEvent) {
if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) { if mouse_event.kind == MouseEventKind::Down(MouseButton::Left) {
let intersect = self.gui_state.lock().button_intersect(Rect::new( let intersect = self.gui_state.lock().button_intersect(Rect::new(
mouse_event.column, mouse_event.column,
@@ -455,7 +508,7 @@ impl InputHandler {
} }
/// Handle mouse button events /// Handle mouse button events
fn mouse_press(&mut self, mouse_event: MouseEvent) { fn mouse_press(&self, mouse_event: MouseEvent) {
match mouse_event.kind { match mouse_event.kind {
MouseEventKind::ScrollUp => self.previous(), MouseEventKind::ScrollUp => self.previous(),
MouseEventKind::ScrollDown => self.next(), MouseEventKind::ScrollDown => self.next(),
@@ -481,7 +534,7 @@ impl InputHandler {
} }
/// Change state to next, depending which panel is currently in focus /// Change state to next, depending which panel is currently in focus
fn next(&mut self) { fn next(&self) {
let mut locked_data = self.app_data.lock(); let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel(); let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel { match selected_panel {
@@ -492,7 +545,7 @@ impl InputHandler {
} }
/// Change state to previous, depending which panel is currently in focus /// Change state to previous, depending which panel is currently in focus
fn previous(&mut self) { fn previous(&self) {
let mut locked_data = self.app_data.lock(); let mut locked_data = self.app_data.lock();
let selected_panel = self.gui_state.lock().get_selected_panel(); let selected_panel = self.gui_state.lock().get_selected_panel();
match selected_panel { match selected_panel {
+13 -3
View File
@@ -167,10 +167,18 @@ async fn main() {
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)] #[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)]
mod tests { mod tests {
use std::{
collections::{HashSet, VecDeque},
vec,
};
use bollard::service::{ContainerSummary, Port}; use bollard::service::{ContainerSummary, Port};
use crate::{ use crate::{
app_data::{AppData, ContainerId, ContainerItem, ContainerPorts, State, StatefulList}, app_data::{
AppData, ContainerId, ContainerItem, ContainerPorts, ContainerStatus, Filter,
RunningState, State, StatefulList,
},
parse_args::CliArgs, parse_args::CliArgs,
}; };
@@ -201,16 +209,18 @@ 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")),
) )
} }
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData { pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
AppData { AppData {
containers: StatefulList::new(containers.to_vec()), containers: StatefulList::new(containers.to_vec()),
hidden_containers: vec![],
error: None, error: None,
sorted_by: None, sorted_by: None,
filter: Filter::new(),
args: gen_args(), args: gen_args(),
} }
} }
+10 -6
View File
@@ -41,15 +41,19 @@ pub mod log_sanitizer {
/// Remove all ansi formatting from a given string and create ratatui Lines /// Remove all ansi formatting from a given string and create ratatui Lines
pub fn remove_ansi<'a>(input: &str) -> Vec<Line<'a>> { pub fn remove_ansi<'a>(input: &str) -> Vec<Line<'a>> {
raw(&categorise_text(input) vec![Line::from(
categorise_text(input)
.into_iter() .into_iter()
.map(|i| i.text) .map(|i| i.text)
.collect::<String>()) .collect::<String>()
.trim()
.to_owned(),
)]
} }
/// create ratatui Lines that exactly match the given strings /// create ratatui Lines that exactly match the given strings
pub fn raw<'a>(input: &str) -> Vec<Line<'a>> { pub fn raw<'a>(input: &str) -> Vec<Line<'a>> {
vec![Line::from(Span::raw(input.to_owned()))] vec![Line::from(input.escape_debug().collect::<String>())]
} }
/// Change from ansi to tui colors /// Change from ansi to tui colors
@@ -62,7 +66,7 @@ pub mod log_sanitizer {
CansiColor::Blue => Color::Blue, CansiColor::Blue => Color::Blue,
CansiColor::Magenta => Color::Magenta, CansiColor::Magenta => Color::Magenta,
CansiColor::Cyan => Color::Cyan, CansiColor::Cyan => Color::Cyan,
CansiColor::White | CansiColor::BrightWhite => Color::White, CansiColor::White | CansiColor::BrightWhite => Color::Gray,
CansiColor::BrightRed => Color::LightRed, CansiColor::BrightRed => Color::LightRed,
CansiColor::BrightGreen => Color::LightGreen, CansiColor::BrightGreen => Color::LightGreen,
CansiColor::BrightYellow => Color::LightYellow, CansiColor::BrightYellow => Color::LightYellow,
@@ -92,7 +96,7 @@ mod tests {
let expected = vec![Line { let expected = vec![Line {
spans: [Span { spans: [Span {
content: std::borrow::Cow::Borrowed( content: std::borrow::Cow::Borrowed(
"\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m", "\\u{1b}[31;47mo\\u{1b}[32;40mx\\u{1b}[33;41mk\\u{1b}[34;42me\\u{1b}[35;43mr\\u{1b}[0m",
), ),
style: Style::default(), style: Style::default(),
}] }]
@@ -111,7 +115,7 @@ mod tests {
spans: vec![ spans: vec![
Span { Span {
content: std::borrow::Cow::Borrowed("o"), content: std::borrow::Cow::Borrowed("o"),
style: Style::default().fg(Color::Red).bg(Color::White), style: Style::default().fg(Color::Red).bg(Color::Gray),
}, },
Span { Span {
content: std::borrow::Cow::Borrowed("x"), content: std::borrow::Cow::Borrowed("x"),
+1095 -758
View File
File diff suppressed because it is too large Load Diff
+32 -29
View File
@@ -163,19 +163,21 @@ pub enum Status {
DockerConnect, DockerConnect,
Error, Error,
Exec, Exec,
Filter,
Help, Help,
Init, Init,
Logs, Logs,
} }
/// Global gui_state, stored in an Arc<Mutex> /// Global gui_state, stored in an Arc<Mutex>
#[derive(Debug, Default, Clone)] #[derive(Debug, Default)]
pub struct GuiState { pub struct GuiState {
delete_container: Option<ContainerId>, delete_container: Option<ContainerId>,
delete_map: HashMap<DeleteButton, Rect>, delete_map: HashMap<DeleteButton, Rect>,
heading_map: HashMap<Header, Rect>, heading_map: HashMap<Header, Rect>,
is_loading: HashSet<Uuid>, loading_handle: Option<JoinHandle<()>>,
loading_index: u8, loading_index: u8,
loading_set: HashSet<Uuid>,
panel_map: HashMap<SelectablePanel, Rect>, panel_map: HashMap<SelectablePanel, Rect>,
selected_panel: SelectablePanel, selected_panel: SelectablePanel,
status: HashSet<Status>, status: HashSet<Status>,
@@ -207,7 +209,7 @@ impl GuiState {
} }
/// Check if a given Rect (a clicked area of 1x1), interacts with any known delete button /// 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> { pub fn button_intersect(&self, rect: Rect) -> Option<DeleteButton> {
self.delete_map self.delete_map
.iter() .iter()
.filter(|i| i.1.intersects(rect)) .filter(|i| i.1.intersects(rect))
@@ -217,7 +219,7 @@ impl GuiState {
} }
/// 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(&self, rect: Rect) -> Option<Header> {
self.heading_map self.heading_map
.iter() .iter()
.filter(|i| i.1.intersects(rect)) .filter(|i| i.1.intersects(rect))
@@ -293,7 +295,7 @@ impl GuiState {
self.status.insert(Status::Exec); self.status.insert(Status::Exec);
} }
pub fn get_exec_mode(&mut self) -> Option<ExecMode> { pub fn get_exec_mode(&self) -> Option<ExecMode> {
self.exec_mode.clone() self.exec_mode.clone()
} }
@@ -325,45 +327,46 @@ impl GuiState {
} else { } else {
self.loading_index += 1; self.loading_index += 1;
} }
self.is_loading.insert(uuid); self.loading_set.insert(uuid);
} }
pub fn is_loading(&self) -> bool {
!self.loading_set.is_empty()
}
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' ' /// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
pub fn get_loading(&self) -> char { pub fn get_loading(&self) -> char {
if self.is_loading.is_empty() { if self.is_loading() {
' '
} else {
FRAMES[usize::from(self.loading_index)] FRAMES[usize::from(self.loading_index)]
} } else {
} ' '
/// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0
fn remove_loading(&mut self, uuid: Uuid) {
self.is_loading.remove(&uuid);
if self.is_loading.is_empty() {
self.loading_index = 0;
} }
} }
/// Animate the loading icon in its own Tokio thread /// Animate the loading icon in its own Tokio thread
pub fn start_loading_animation( /// This should only be able to executed once, rather than multiple spawns
gui_state: &Arc<Mutex<Self>>, pub fn start_loading_animation(gui_state: &Arc<Mutex<Self>>, loading_uuid: Uuid) {
loading_uuid: Uuid, if !gui_state.lock().is_loading() {
) -> JoinHandle<()> { let inner_state = Arc::clone(gui_state);
gui_state.lock().next_loading(loading_uuid); gui_state.lock().loading_handle = Some(tokio::spawn(async move {
let gui_state = Arc::clone(gui_state);
tokio::spawn(async move {
loop { loop {
tokio::time::sleep(std::time::Duration::from_millis(100)).await; tokio::time::sleep(std::time::Duration::from_millis(100)).await;
gui_state.lock().next_loading(loading_uuid); inner_state.lock().next_loading(loading_uuid);
} }
}) }));
}
gui_state.lock().next_loading(loading_uuid);
} }
/// Stop the loading_spin function, and reset gui loading status /// Stop the loading_spin function, and reset gui loading status
pub fn stop_loading_animation(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) { pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
handle.abort(); self.loading_set.remove(&loading_uuid);
self.remove_loading(loading_uuid); if self.loading_set.is_empty() {
self.loading_index = 0;
if let Some(h) = &self.loading_handle {
h.abort();
}
self.loading_handle = None;
}
} }
/// Set info box content /// Set info box content
+17 -5
View File
@@ -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>>,
@@ -64,7 +66,6 @@ impl Ui {
is_running: Arc<AtomicBool>, is_running: Arc<AtomicBool>,
) { ) {
if let Ok(mut terminal) = Self::setup_terminal() { if let Ok(mut terminal) = Self::setup_terminal() {
// let args = app_data.lock().args.clone();
let cursor_position = terminal.get_cursor().unwrap_or_default(); let cursor_position = terminal.get_cursor().unwrap_or_default();
let mut ui = Self { let mut ui = Self {
app_data, app_data,
@@ -264,14 +265,20 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
/// Draw the main ui to a frame of the terminal /// Draw the main ui to a frame of the terminal
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) { fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
let fd = FrameData::from((app_data.lock(), gui_state.lock())); let fd = FrameData::from((app_data.lock(), gui_state.lock()));
let contains_filter = gui_state.lock().status_contains(&[Status::Filter]);
let whole_constraints = if contains_filter {
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
} else {
vec![Constraint::Max(1), Constraint::Min(1)]
};
let whole_layout = Layout::default() let whole_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Max(1), Constraint::Min(1)].as_ref()) .constraints(whole_constraints)
.split(f.size()); .split(f.size());
// Split into 3, containers+controls, logs, then graphs // Split into 3, containers+controls, logs, then graphs
// This one is the issue!
let upper_main = Layout::default() let upper_main = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Max(fd.height), Constraint::Min(1)].as_ref()) .constraints([Constraint::Max(fd.height), Constraint::Min(1)].as_ref())
@@ -306,6 +313,11 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state); draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state);
// Draw filter bar
if let Some(rect) = whole_layout.get(2) {
draw_blocks::filter_bar(*rect, f, app_data);
}
if let Some(id) = fd.delete_confirm.as_ref() { if let Some(id) = fd.delete_confirm.as_ref() {
app_data.lock().get_container_name_by_id(id).map_or_else( app_data.lock().get_container_name_by_id(id).map_or_else(
|| { || {
@@ -320,8 +332,8 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
} }
// only draw commands + charts if there are containers // only draw commands + charts if there are containers
if fd.has_containers { if let Some(rect) = top_panel.get(1) {
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state); draw_blocks::commands(app_data, *rect, f, &fd, gui_state);
// Can calculate the max string length here, and then use that to keep the ports section as small as possible (+4 for some padding + border) // Can calculate the max string length here, and then use that to keep the ports section as small as possible (+4 for some padding + border)
let max_lens = app_data.lock().get_longest_port(); let max_lens = app_data.lock().get_longest_port();