feat: Added ports section, closes #21

This commit is contained in:
Jack Wills
2024-01-18 00:25:09 +00:00
parent b6fd35022a
commit 65a1afcb06
8 changed files with 518 additions and 61 deletions
+301 -38
View File
@@ -1,7 +1,7 @@
use parking_lot::Mutex;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
style::{Color, Modifier, Style, Stylize},
symbols,
text::{Line, Span},
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
pub fn chart(f: &mut Frame, area: Rect, app_data: &Arc<Mutex<AppData>>) {
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,
max: &'a T,
) -> Chart<'a> {
let title_color = match state {
State::Running => Color::Green,
_ => state.get_color(),
};
let title_color = state.get_color();
let label_color = match state {
State::Running => ORANGE,
_ => state.get_color(),
@@ -966,8 +1018,8 @@ mod tests {
use crate::{
app_data::{
AppData, ContainerId, ContainerImage, ContainerName, Header, SortedOrder, State,
StatefulList,
AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts, Header,
SortedOrder, State, StatefulList,
},
app_error::AppError,
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 //
// **************** //
#[test]
// 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 mut setup = test_setup(w, h, true, true);
insert_chart_data(&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 = [
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
"╭ 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_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 │",
"│ ││ delete │",
"│ ││ │",
"│ ││ │",
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯",
"╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
"│ line 1 │",
"│ line 2 │",
"│▶ line 3 │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────",
"╭───────────────────────────────── cpu 03.00% ─────────────────────────────────╮╭────────────────────────────── memory 30.00 kB ───────────────────────────────╮",
"10.00%│ •••••• ││100.00 kB│ •••••• │",
"│ │••••• ••• ││ │••••• ••• ",
"│ │ ││ │ │",
"╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯",
"╭ 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_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 │",
"│ ││ delete │",
"│ ││ │",
"│ ││ │",
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯",
"╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮",
"│ line 1 │",
"│ line 2 │",
"│▶ line 3 │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"│ │",
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
"───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────",
"│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│",
" ••• ││ │ ••• ││ 8001 ",
"│ │•• ••• ││ │•• ••• ││127.0.0.1 8003 8003",
"│ │ ││ │ ││",
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
];
setup
.terminal
@@ -2744,7 +3007,7 @@ mod tests {
let index = row_index * usize::from(w) + char_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]);
let lower_split = if fd.has_containers {
vec![Constraint::Percentage(75), Constraint::Percentage(25)]
vec![Constraint::Percentage(70), Constraint::Percentage(20)]
} else {
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
if fd.has_containers {
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 {