Merge branch 'feat/ports' into dev

This commit is contained in:
Jack Wills
2024-01-18 14:31:48 +00:00
8 changed files with 518 additions and 61 deletions
+55 -7
View File
@@ -4,6 +4,7 @@ use std::{
fmt, fmt,
}; };
use bollard::service::Port;
use ratatui::{ use ratatui::{
style::Color, style::Color,
widgets::{ListItem, ListState}, widgets::{ListItem, ListState},
@@ -100,6 +101,47 @@ macro_rules! unit_struct {
unit_struct!(ContainerName); unit_struct!(ContainerName);
unit_struct!(ContainerImage); unit_struct!(ContainerImage);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerPorts {
pub ip: Option<String>,
pub private: u16,
pub public: Option<u16>,
}
impl From<&Port> for ContainerPorts {
fn from(value: &Port) -> Self {
Self {
ip: value.ip.clone(),
private: value.private_port,
public: value.public_port,
}
}
}
impl ContainerPorts {
pub fn len_ip(&self) -> usize {
self.ip.as_ref().unwrap_or(&String::new()).chars().count()
}
pub fn len_private(&self) -> usize {
format!("{}", self.private).chars().count()
}
pub fn len_public(&self) -> usize {
format!("{}", self.public.unwrap_or_default())
.chars()
.count()
}
pub fn print(&self) -> (String, String, String) {
(
self.ip
.as_ref()
.map_or(String::new(), std::borrow::ToOwned::to_owned),
format!("{}", self.private),
self.public.map_or(String::new(), |s| s.to_string()),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatefulList<T> { pub struct StatefulList<T> {
pub state: ListState, pub state: ListState,
@@ -247,7 +289,7 @@ pub enum DockerControls {
Restart, Restart,
Start, Start,
Stop, Stop,
Unpause, Resume,
Delete, Delete,
} }
@@ -259,7 +301,7 @@ impl DockerControls {
Self::Start => Color::Green, Self::Start => Color::Green,
Self::Stop => Color::Red, Self::Stop => Color::Red,
Self::Delete => Color::Gray, Self::Delete => Color::Gray,
Self::Unpause => Color::Blue, Self::Resume => Color::Blue,
} }
} }
@@ -267,7 +309,7 @@ impl DockerControls {
pub fn gen_vec(state: State) -> Vec<Self> { pub fn gen_vec(state: State) -> Vec<Self> {
match state { match state {
State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete], State::Dead | State::Exited => vec![Self::Start, Self::Restart, Self::Delete],
State::Paused => vec![Self::Unpause, 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],
@@ -283,7 +325,7 @@ impl fmt::Display for DockerControls {
Self::Restart => "restart", Self::Restart => "restart",
Self::Start => "start", Self::Start => "start",
Self::Stop => "stop", Self::Stop => "stop",
Self::Unpause => "resume", Self::Resume => "resume",
}; };
write!(f, "{disp}") write!(f, "{disp}")
} }
@@ -484,21 +526,23 @@ impl Logs {
/// Info for each container /// Info for each container
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerItem { pub struct ContainerItem {
pub created: u64,
pub cpu_stats: VecDeque<CpuStats>, pub cpu_stats: VecDeque<CpuStats>,
pub created: u64,
pub docker_controls: StatefulList<DockerControls>, pub docker_controls: StatefulList<DockerControls>,
pub id: ContainerId, pub id: ContainerId,
pub image: ContainerImage, pub image: ContainerImage,
pub is_oxker: bool,
pub last_updated: u64, pub last_updated: u64,
pub logs: Logs, pub logs: Logs,
pub mem_limit: ByteStats, pub mem_limit: ByteStats,
pub mem_stats: VecDeque<ByteStats>, pub mem_stats: VecDeque<ByteStats>,
pub name: ContainerName, pub name: ContainerName,
// todo remove option, can be empty vec
pub ports: Vec<ContainerPorts>,
pub rx: ByteStats, pub rx: ByteStats,
pub state: State, pub state: State,
pub status: String, pub status: String,
pub tx: ByteStats, pub tx: ByteStats,
pub is_oxker: bool,
} }
/// Basic display information, for when running in debug mode /// Basic display information, for when running in debug mode
@@ -516,6 +560,7 @@ impl fmt::Display for ContainerItem {
} }
impl ContainerItem { impl ContainerItem {
#[allow(clippy::too_many_arguments)]
/// Create a new container item /// Create a new container item
pub fn new( pub fn new(
created: u64, created: u64,
@@ -523,14 +568,16 @@ impl ContainerItem {
image: String, image: String,
is_oxker: bool, is_oxker: bool,
name: String, name: String,
ports: Vec<ContainerPorts>,
state: State, state: State,
status: String, status: String,
) -> 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();
Self { Self {
created,
cpu_stats: VecDeque::with_capacity(60), cpu_stats: VecDeque::with_capacity(60),
created,
docker_controls, docker_controls,
id, id,
image: image.into(), image: image.into(),
@@ -540,6 +587,7 @@ impl ContainerItem {
mem_limit: ByteStats::default(), mem_limit: ByteStats::default(),
mem_stats: VecDeque::with_capacity(60), mem_stats: VecDeque::with_capacity(60),
name: name.into(), name: name.into(),
ports,
rx: ByteStats::default(), rx: ByteStats::default(),
state, state,
status, status,
+131 -6
View File
@@ -254,6 +254,51 @@ impl AppData {
.and_then(|i| self.containers.items.get(i)) .and_then(|i| self.containers.items.get(i))
} }
/// Find the longest port when it's transformed into a string, defaults are header lens (ip, private, public)
pub fn get_longest_port(&self) -> (usize, usize, usize) {
let mut longest_ip = 5;
let mut longest_private = 10;
let mut longest_public = 9;
for item in &self.containers.items {
// if let Some(ports) = item.ports.as_ref() {
longest_ip = longest_ip.max(
item.ports
.iter()
.map(ContainerPorts::len_ip)
.max()
.unwrap_or(3),
);
longest_private = longest_private.max(
item.ports
.iter()
.map(ContainerPorts::len_private)
.max()
.unwrap_or(8),
);
longest_public = longest_public.max(
item.ports
.iter()
.map(ContainerPorts::len_public)
.max()
.unwrap_or(6),
);
}
// }
(longest_ip, longest_private, longest_public)
// )
}
/// Get Option of the current selected container's ports, sorted by private port
pub fn get_selected_ports(&mut self) -> Option<(Vec<ContainerPorts>, State)> {
if let Some(item) = self.get_mut_selected_container() {
let mut ports = item.ports.clone();
ports.sort_by(|a, b| a.private.cmp(&b.private));
return Some((ports, item.state));
}
None
}
/// Get mutable Option of the current selected container /// Get mutable Option of the current selected container
fn get_mut_selected_container(&mut self) -> Option<&mut ContainerItem> { fn get_mut_selected_container(&mut self) -> Option<&mut ContainerItem> {
self.containers self.containers
@@ -571,6 +616,10 @@ impl AppData {
}) })
}); });
let ports = i.ports.as_ref().map_or(vec![], |i| {
i.iter().map(ContainerPorts::from).collect::<Vec<_>>()
});
let id = ContainerId::from(id.as_str()); let id = ContainerId::from(id.as_str());
let is_oxker = i let is_oxker = i
@@ -611,13 +660,17 @@ impl AppData {
}; };
item.state = state; item.state = state;
}; };
item.ports = ports;
if item.image.get() != image { if item.image.get() != image {
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 Vec
let container = let container = ContainerItem::new(
ContainerItem::new(created, id, image, is_oxker, name, state, status); created, id, image, is_oxker, name, ports, state, status,
);
self.containers.items.push(container); self.containers.items.push(container);
} }
} }
@@ -1325,6 +1378,7 @@ mod tests {
"image_1".to_owned(), "image_1".to_owned(),
false, false,
"container_1".to_owned(), "container_1".to_owned(),
vec![],
state, state,
"Up 1 hour".to_owned(), "Up 1 hour".to_owned(),
) )
@@ -1356,7 +1410,7 @@ mod tests {
test_state( test_state(
State::Paused, State::Paused,
&mut vec![ &mut vec![
DockerControls::Unpause, DockerControls::Resume,
DockerControls::Stop, DockerControls::Stop,
DockerControls::Delete, DockerControls::Delete,
], ],
@@ -1652,9 +1706,9 @@ mod tests {
); );
} }
// ********** // // ************* //
// Chart data // // Header Widths //
// ********** // // ************* //
#[test] #[test]
/// Header widths return correctly /// Header widths return correctly
@@ -1677,6 +1731,77 @@ mod tests {
assert_eq!(result, expected); assert_eq!(result, expected);
} }
// ************* //
// Header Widths //
// ************* //
#[test]
/// Returns selected containers ports ordered by private ip
fn test_app_data_get_selected_ports() {
let (_ids, containers) = gen_containers();
let mut app_data = gen_appdata(&containers);
app_data.containers.items[0].ports.push(ContainerPorts {
ip: None,
private: 10,
public: Some(1),
});
app_data.containers.items[0].ports.push(ContainerPorts {
ip: None,
private: 11,
public: Some(3),
});
app_data.containers.items[0].ports.push(ContainerPorts {
ip: None,
private: 4,
public: Some(2),
});
// No containers selected
let result = app_data.get_selected_ports();
assert!(result.is_none());
// Selected container & ports
app_data.containers_start();
let result = app_data.get_selected_ports();
assert_eq!(
result,
Some((
vec![
ContainerPorts {
ip: None,
private: 4,
public: Some(2)
},
ContainerPorts {
ip: None,
private: 10,
public: Some(1)
},
ContainerPorts {
ip: None,
private: 11,
public: Some(3)
},
ContainerPorts {
ip: None,
private: 8001,
public: None
}
],
State::Running
))
);
// Selected container & no ports
app_data.containers_start();
app_data.containers.items[0].ports = vec![];
let result = app_data.get_selected_ports();
assert_eq!(result, Some((vec![], State::Running)));
}
// ************** // // ************** //
// Update mtehods // // Update mtehods //
// ************** // // ************** //
+1 -1
View File
@@ -14,6 +14,6 @@ pub enum DockerMessage {
Restart(ContainerId), Restart(ContainerId),
Start(ContainerId), Start(ContainerId),
Stop(ContainerId), Stop(ContainerId),
Unpause(ContainerId), Resume(ContainerId),
Update, Update,
} }
+2 -2
View File
@@ -387,11 +387,11 @@ impl DockerData {
}); });
self.update_everything().await; self.update_everything().await;
} }
DockerMessage::Unpause(id) => { DockerMessage::Resume(id) => {
tokio::spawn(async move { tokio::spawn(async move {
let handle = GuiState::start_loading_animation(&gui_state, uuid); let handle = 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::Unpause, &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(&handle, uuid);
}); });
+2 -2
View File
@@ -286,8 +286,8 @@ impl InputHandler {
DockerControls::Pause => { DockerControls::Pause => {
self.docker_tx.send(DockerMessage::Pause(id)).await.ok() self.docker_tx.send(DockerMessage::Pause(id)).await.ok()
} }
DockerControls::Unpause => { DockerControls::Resume => {
self.docker_tx.send(DockerMessage::Unpause(id)).await.ok() self.docker_tx.send(DockerMessage::Resume(id)).await.ok()
} }
DockerControls::Start => { DockerControls::Start => {
self.docker_tx.send(DockerMessage::Start(id)).await.ok() self.docker_tx.send(DockerMessage::Start(id)).await.ok()
+13 -3
View File
@@ -168,10 +168,10 @@ 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 bollard::service::ContainerSummary; use bollard::service::{ContainerSummary, Port};
use crate::{ use crate::{
app_data::{AppData, ContainerId, ContainerItem, State, StatefulList}, app_data::{AppData, ContainerId, ContainerItem, ContainerPorts, State, StatefulList},
parse_args::CliArgs, parse_args::CliArgs,
}; };
@@ -197,6 +197,11 @@ mod tests {
format!("image_{index}"), format!("image_{index}"),
false, false,
format!("container_{index}"), format!("container_{index}"),
vec![ContainerPorts {
ip: None,
private: u16::try_from(index).unwrap_or(1) + 8000,
public: None,
}],
State::Running, State::Running,
format!("Up {index} hour"), format!("Up {index} hour"),
) )
@@ -231,7 +236,12 @@ mod tests {
image_id: Some(format!("{index}")), image_id: Some(format!("{index}")),
command: None, command: None,
created: Some(i64::try_from(index).unwrap()), created: Some(i64::try_from(index).unwrap()),
ports: None, ports: Some(vec![Port {
ip: None,
private_port: u16::try_from(index).unwrap_or(1) + 8000,
public_port: None,
typ: None,
}]),
size_rw: None, size_rw: None,
size_root_fs: None, size_root_fs: None,
labels: None, labels: None,
+301 -38
View File
@@ -1,7 +1,7 @@
use parking_lot::Mutex; use parking_lot::Mutex;
use ratatui::{ use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style, Stylize},
symbols, symbols,
text::{Line, Span}, text::{Line, Span},
widgets::{ widgets::{
@@ -269,6 +269,61 @@ pub fn logs(
} }
} }
// Display the ports in a formatted list
pub fn ports(
f: &mut Frame,
area: Rect,
app_data: &Arc<Mutex<AppData>>,
max_lens: (usize, usize, usize),
) {
if let Some(ports) = app_data.lock().get_selected_ports() {
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title_alignment(Alignment::Center)
.title(Span::styled(
" ports ",
Style::default()
.fg(ports.1.get_color())
.add_modifier(Modifier::BOLD),
));
let (ip, private, public) = max_lens;
if ports.0.is_empty() {
let paragraph = Paragraph::new(Span::from("no ports").add_modifier(Modifier::BOLD))
.alignment(Alignment::Center)
.block(block);
f.render_widget(paragraph, area);
} else {
let mut output = vec![Line::from(
Span::from(format!(
"{:>ip$}{:>private$}{:>public$}",
"ip", "private", "public"
))
.fg(Color::Yellow),
)];
for (index, item) in ports.0.iter().enumerate() {
let fg = if index % 2 == 0 {
Color::White
} else {
Color::Magenta
};
let strings = item.print();
let line = vec![
Span::from(format!("{:>ip$}", strings.0)).fg(fg),
Span::from(format!("{:>private$}", strings.1)).fg(fg),
Span::from(format!("{:>public$}", strings.2)).fg(fg),
];
output.push(Line::from(line));
}
let paragraph = Paragraph::new(output).block(block);
f.render_widget(paragraph, area);
}
}
}
/// Draw the cpu + mem charts /// Draw the cpu + mem charts
pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc<Mutex<AppData>>) { pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc<Mutex<AppData>>) {
if let Some((cpu, mem)) = app_data.lock().get_chart_data() { if let Some((cpu, mem)) = app_data.lock().get_chart_data() {
@@ -307,10 +362,7 @@ fn make_chart<'a, T: Stats + Display>(
current: &'a T, current: &'a T,
max: &'a T, max: &'a T,
) -> Chart<'a> { ) -> Chart<'a> {
let title_color = match state { let title_color = state.get_color();
State::Running => Color::Green,
_ => 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(),
@@ -966,8 +1018,8 @@ mod tests {
use crate::{ use crate::{
app_data::{ app_data::{
AppData, ContainerId, ContainerImage, ContainerName, Header, SortedOrder, State, AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, Header,
StatefulList, 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},
@@ -2687,49 +2739,260 @@ mod tests {
} }
} }
#[test]
// Port section when container has no ports
fn test_draw_blocks_ports_no_ports() {
let (w, h) = (30, 8);
let mut setup = test_setup(w, h, true, true);
setup.app_data.lock().containers.items[0].ports = vec![];
let max_lens = setup.app_data.lock().get_longest_port();
setup
.terminal
.draw(|f| {
super::ports(f, setup.area, &setup.app_data, max_lens);
})
.unwrap();
let expected = [
"╭────────── ports ───────────╮",
"│ no ports │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"╰────────────────────────────╯",
];
let result = &setup.terminal.backend().buffer().content;
for (row_index, row) in expected.iter().enumerate() {
for (char_index, expected_char) in row.chars().enumerate() {
let index = row_index * usize::from(w) + char_index;
let result_cell = &result[index];
assert_eq!(expected_char.to_string(), result_cell.symbol());
if row_index == 0 && !BORDER_CHARS.contains(&result_cell.symbol()) {
assert_eq!(result_cell.fg, Color::Green);
assert_eq!(result_cell.modifier, Modifier::BOLD);
} else {
assert_eq!(result_cell.fg, Color::Reset);
}
}
}
}
#[test]
// Port section when container has multiple ports
fn test_draw_blocks_ports_multiple_ports() {
let (w, h) = (32, 8);
let mut setup = test_setup(w, h, true, true);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: None,
private: 8002,
public: None,
});
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some("127.0.0.1".to_owned()),
private: 8003,
public: Some(8003),
});
let max_lens = setup.app_data.lock().get_longest_port();
setup
.terminal
.draw(|f| {
super::ports(f, setup.area, &setup.app_data, max_lens);
})
.unwrap();
let expected = [
"╭─────────── ports ────────────╮",
"│ ip private public │",
"│ 8001 │",
"│ 8002 │",
"│127.0.0.1 8003 8003 │",
"│ │",
"│ │",
"╰──────────────────────────────╯",
];
let result = &setup.terminal.backend().buffer().content;
for (row_index, row) in expected.iter().enumerate() {
for (char_index, expected_char) in row.chars().enumerate() {
let index = row_index * usize::from(w) + char_index;
let result_cell = &result[index];
assert_eq!(expected_char.to_string(), result_cell.symbol());
let result_cell_as_char = result_cell
.symbol()
.chars()
.next()
.unwrap()
.is_ascii_alphanumeric();
if row_index == 0 && result_cell_as_char {
assert_eq!(result_cell.fg, Color::Green);
}
if row_index == 1 && result_cell_as_char {
assert_eq!(result_cell.fg, Color::Yellow);
}
if row_index == 2 && result_cell_as_char {
assert_eq!(result_cell.fg, Color::White);
}
if row_index == 3 && result_cell_as_char {
assert_eq!(result_cell.fg, Color::Magenta);
}
if row_index == 4 && result_cell_as_char {
assert_eq!(result_cell.fg, Color::White);
}
}
}
}
#[test]
// Port section title color correct dependant on state
fn test_draw_blocks_ports_container_state() {
let (w, h) = (32, 8);
let mut setup = test_setup(w, h, true, true);
let max_lens = setup.app_data.lock().get_longest_port();
setup.app_data.lock().containers.items[0].state = State::Paused;
setup
.terminal
.draw(|f| {
super::ports(f, setup.area, &setup.app_data, max_lens);
})
.unwrap();
let expected = [
"╭─────────── ports ────────────╮",
"│ ip private public │",
"│ 8001 │",
"│ │",
"│ │",
"│ │",
"│ │",
"╰──────────────────────────────╯",
];
let result = &setup.terminal.backend().buffer().content;
for (row_index, row) in expected.iter().enumerate() {
for (char_index, expected_char) in row.chars().enumerate() {
let index = row_index * usize::from(w) + char_index;
let result_cell = &result[index];
assert_eq!(expected_char.to_string(), result_cell.symbol());
if row_index == 0
&& result_cell
.symbol()
.chars()
.next()
.unwrap()
.is_ascii_alphanumeric()
{
assert_eq!(result_cell.fg, Color::Yellow);
}
}
}
setup.app_data.lock().containers.items[0].state = State::Dead;
setup
.terminal
.draw(|f| {
super::ports(f, setup.area, &setup.app_data, max_lens);
})
.unwrap();
let expected = [
"╭─────────── ports ────────────╮",
"│ ip private public │",
"│ 8001 │",
"│ │",
"│ │",
"│ │",
"│ │",
"╰──────────────────────────────╯",
];
let result = &setup.terminal.backend().buffer().content;
for (row_index, row) in expected.iter().enumerate() {
for (char_index, expected_char) in row.chars().enumerate() {
let index = row_index * usize::from(w) + char_index;
let result_cell = &result[index];
assert_eq!(expected_char.to_string(), result_cell.symbol());
if row_index == 0
&& result_cell
.symbol()
.chars()
.next()
.unwrap()
.is_ascii_alphanumeric()
{
assert_eq!(result_cell.fg, Color::Red);
}
}
}
}
// *************** // // *************** //
// The whole layout // // The whole layout //
// **************** // // **************** //
#[test] #[test]
// Check that the whole layout is drawn correctly // Check that the whole layout is drawn correctly
fn test_draw_blocks_the_whole_layout() { fn test_draw_blocks_whole_layout() {
let (w, h) = (160, 30); let (w, h) = (160, 30);
let mut setup = test_setup(w, h, true, true); let mut setup = test_setup(w, h, true, true);
insert_chart_data(&setup); insert_chart_data(&setup);
insert_logs(&setup); insert_logs(&setup);
setup.app_data.lock().containers.items[0]
.ports
.push(ContainerPorts {
ip: Some("127.0.0.1".to_owned()),
private: 8003,
public: Some(8003),
});
let expected = [ let expected = [
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
"╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮",
"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │", "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │",
"│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │", "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │",
"│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │", "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │",
"│ ││ delete │", "│ ││ delete │",
"│ ││ │", "│ ││ │",
"│ ││ │", "│ ││ │",
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯",
"╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", "╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
"│ line 1 │", "│ line 1 │",
"│ line 2 │", "│ line 2 │",
"│▶ line 3 │", "│▶ line 3 │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "│ │",
"│ │", "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────", "───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────",
"╭───────────────────────────────── cpu 03.00% ─────────────────────────────────╮╭────────────────────────────── memory 30.00 kB ───────────────────────────────╮", "│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│",
"10.00%│ •••••• ││100.00 kB│ •••••• │", " ••• ││ │ ••• ││ 8001 ",
"│ │••••• ••• ││ │••••• ••• ", "│ │•• ••• ││ │•• ••• ││127.0.0.1 8003 8003",
"│ │ ││ │ │", "│ │ ││ │ ││",
"╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯", "╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
]; ];
setup setup
.terminal .terminal
@@ -2744,7 +3007,7 @@ mod tests {
let index = row_index * usize::from(w) + char_index; let index = row_index * usize::from(w) + char_index;
let result_cell = &result[index]; let result_cell = &result[index];
assert_eq!(expected_char.to_string(), result_cell.symbol()); assert_eq!(result_cell.symbol(), expected_char.to_string(),);
} }
} }
} }
+13 -2
View File
@@ -286,7 +286,7 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
.split(upper_main[0]); .split(upper_main[0]);
let lower_split = if fd.has_containers { let lower_split = if fd.has_containers {
vec![Constraint::Percentage(75), Constraint::Percentage(25)] vec![Constraint::Percentage(70), Constraint::Percentage(20)]
} else { } else {
vec![Constraint::Percentage(100)] vec![Constraint::Percentage(100)]
}; };
@@ -319,7 +319,18 @@ 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 fd.has_containers {
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state); draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
draw_blocks::chart(f, lower_main[1], app_data);
// 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 ports_len = u16::try_from(max_lens.0 + max_lens.1 + max_lens.2 + 2).unwrap_or(26);
let lower = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(1), Constraint::Max(ports_len)])
.split(lower_main[1]);
draw_blocks::chart(f, lower[0], app_data);
draw_blocks::ports(f, lower[1], app_data, max_lens);
} }
if let Some((text, instant)) = fd.info_text { if let Some((text, instant)) = fd.info_text {