557 lines
18 KiB
Rust
557 lines
18 KiB
Rust
use std::sync::Arc;
|
|
|
|
use parking_lot::Mutex;
|
|
use ratatui::{
|
|
layout::{Constraint, Rect},
|
|
style::Style,
|
|
widgets::{Block, BorderType, Borders},
|
|
};
|
|
|
|
use crate::config::AppColors;
|
|
|
|
use super::{FrameData, GuiState, SelectablePanel, Status, gui_state::Region};
|
|
|
|
pub mod chart_bandwidth;
|
|
pub mod chart_cpu_mem;
|
|
pub mod commands;
|
|
pub mod containers;
|
|
pub mod delete_confirm;
|
|
pub mod error;
|
|
pub mod filter;
|
|
pub mod headers;
|
|
pub mod help;
|
|
pub mod info;
|
|
pub mod inspect;
|
|
pub mod logs;
|
|
pub mod popup;
|
|
pub mod ports;
|
|
pub mod search_logs;
|
|
|
|
pub const NAME_TEXT: &str = r#" 88
|
|
88
|
|
,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba,
|
|
a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8
|
|
8b d8 )888( 8888( 8PP""""""" 88
|
|
"8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88
|
|
`"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 "#;
|
|
|
|
pub const NAME: &str = env!("CARGO_PKG_NAME");
|
|
pub const REPO: &str = env!("CARGO_PKG_REPOSITORY");
|
|
pub const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
|
pub const MARGIN: &str = " ";
|
|
pub const SELECT_ARROW: &str = "▶ ";
|
|
pub const LEFT_ARROW: &str = "←";
|
|
pub const RIGHT_ARROW: &str = "→";
|
|
pub const DOWN_ARROW: &str = "↓";
|
|
pub const UP_ARROW: &str = "↑";
|
|
pub const CIRCLE: &str = "⚪ ";
|
|
|
|
#[cfg(not(test))]
|
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
#[cfg(test)]
|
|
pub const VERSION: &str = "0.00.000";
|
|
|
|
pub const CONSTRAINT_50_50: [Constraint; 2] =
|
|
[Constraint::Percentage(50), Constraint::Percentage(50)];
|
|
pub const CONSTRAINT_100: [Constraint; 1] = [Constraint::Percentage(100)];
|
|
pub const CONSTRAINT_POPUP: [Constraint; 5] = [
|
|
Constraint::Min(2),
|
|
Constraint::Max(1),
|
|
Constraint::Max(1),
|
|
Constraint::Max(3),
|
|
Constraint::Min(1),
|
|
];
|
|
|
|
pub const CONSTRAINT_BUTTONS: [Constraint; 5] = [
|
|
Constraint::Percentage(10),
|
|
Constraint::Percentage(35),
|
|
Constraint::Percentage(10),
|
|
Constraint::Percentage(35),
|
|
Constraint::Percentage(10),
|
|
];
|
|
|
|
/// From a given &str, return the maximum number of chars on a single line
|
|
pub fn max_line_width(text: &str) -> usize {
|
|
text.lines()
|
|
.map(|i| i.chars().count())
|
|
.max()
|
|
.unwrap_or_default()
|
|
}
|
|
/// Generate block, add a border if is the selected panel,
|
|
/// add custom title based on state of each panel
|
|
fn generate_block<'a>(
|
|
area: Rect,
|
|
colors: AppColors,
|
|
fd: &FrameData,
|
|
gui_state: &Arc<Mutex<GuiState>>,
|
|
panel: SelectablePanel,
|
|
) -> Block<'a> {
|
|
gui_state
|
|
.lock()
|
|
.update_region_map(Region::Panel(panel), area);
|
|
|
|
let mut title = match panel {
|
|
SelectablePanel::Containers => {
|
|
format!("{}{}", panel.title(), fd.container_title)
|
|
}
|
|
SelectablePanel::Logs => {
|
|
format!("{}{}", panel.title(), fd.log_title)
|
|
}
|
|
SelectablePanel::Commands => String::new(),
|
|
};
|
|
if !title.is_empty() {
|
|
title = format!(" {title} ");
|
|
}
|
|
let mut block = Block::default()
|
|
.borders(Borders::ALL)
|
|
.border_type(BorderType::Rounded)
|
|
.title(ratatui::text::Line::from(title).left_aligned());
|
|
|
|
if panel == SelectablePanel::Logs
|
|
&& let Some(x) = fd.scroll_title.as_ref()
|
|
{
|
|
block = block
|
|
.title_bottom(x.to_owned())
|
|
.title_alignment(ratatui::layout::Alignment::Right);
|
|
}
|
|
if !fd.status.contains(&Status::Filter) {
|
|
if fd.selected_panel == panel {
|
|
block = block.border_style(Style::default().fg(colors.borders.selected));
|
|
} else {
|
|
block = block.border_style(Style::default().fg(colors.borders.unselected));
|
|
}
|
|
}
|
|
block
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used)]
|
|
pub mod tests {
|
|
|
|
use std::{
|
|
net::{IpAddr, Ipv4Addr},
|
|
sync::Arc,
|
|
};
|
|
|
|
use insta::assert_snapshot;
|
|
use parking_lot::Mutex;
|
|
use ratatui::{Terminal, backend::TestBackend, layout::Rect, style::Color};
|
|
|
|
use crate::{
|
|
app_data::{AppData, ContainerId, ContainerImage, ContainerName, ContainerPorts},
|
|
app_error::AppError,
|
|
tests::{gen_appdata, gen_containers},
|
|
ui::{GuiState, Rerender, Status, draw_frame},
|
|
};
|
|
|
|
use super::FrameData;
|
|
|
|
pub struct TuiTestSetup {
|
|
pub app_data: Arc<Mutex<AppData>>,
|
|
pub gui_state: Arc<Mutex<GuiState>>,
|
|
pub fd: FrameData,
|
|
pub area: Rect,
|
|
pub terminal: Terminal<TestBackend>,
|
|
pub ids: Vec<ContainerId>,
|
|
}
|
|
|
|
pub const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"];
|
|
pub const COLOR_RX: Color = Color::Rgb(255, 233, 193);
|
|
pub const COLOR_TX: Color = Color::Rgb(205, 140, 140);
|
|
pub const COLOR_ORANGE: Color = Color::Rgb(255, 178, 36);
|
|
|
|
/// Create a FrameData struct from two Arc<mutex>'s, instead of from UI
|
|
impl From<(&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)> for FrameData {
|
|
fn from(data: (&Arc<Mutex<AppData>>, &Arc<Mutex<GuiState>>)) -> Self {
|
|
let (mut app_data, gui_data) = (data.0.lock(), data.1.lock());
|
|
|
|
let (filter_by, filter_term) = app_data.get_filter();
|
|
Self {
|
|
chart_data: app_data.get_chart_data(),
|
|
color_logs: app_data.config.color_logs,
|
|
columns: app_data.get_width(),
|
|
container_title: app_data.get_container_title(),
|
|
delete_confirm: gui_data.get_delete_container(),
|
|
filter_by,
|
|
filter_term: filter_term.cloned(),
|
|
has_containers: app_data.get_container_len() > 0,
|
|
has_error: app_data.get_error(),
|
|
show_logs: gui_data.get_show_logs(),
|
|
info_text: gui_data.info_box_text.clone(),
|
|
is_loading: gui_data.is_loading(),
|
|
log_search: app_data.gen_log_search(),
|
|
loading_icon: gui_data.get_loading().to_string(),
|
|
log_height: gui_data.get_log_height(),
|
|
log_title: app_data.get_log_title(),
|
|
scroll_title: app_data.get_scroll_title(gui_data.get_screen_width()),
|
|
port_max_lens: app_data.get_longest_port(),
|
|
ports: app_data.get_selected_ports(),
|
|
selected_panel: gui_data.get_selected_panel(),
|
|
sorted_by: app_data.get_sorted(),
|
|
status: gui_data.get_status(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate state to be used in *most* gui tests
|
|
pub fn test_setup(w: u16, h: u16, control_start: bool, container_start: bool) -> TuiTestSetup {
|
|
let backend = TestBackend::new(w, h);
|
|
let terminal = Terminal::new(backend).unwrap();
|
|
|
|
let (ids, containers) = gen_containers();
|
|
let mut app_data = gen_appdata(&containers);
|
|
if control_start {
|
|
app_data.docker_controls_start();
|
|
}
|
|
if container_start {
|
|
app_data.containers_start();
|
|
}
|
|
|
|
let redraw = Arc::new(Rerender::new());
|
|
let gui_state = GuiState::new(&redraw, app_data.config.show_logs);
|
|
|
|
let app_data = Arc::new(Mutex::new(app_data));
|
|
let gui_state = Arc::new(Mutex::new(gui_state));
|
|
let fd = FrameData::from((&app_data, &gui_state));
|
|
let area = Rect::new(0, 0, w, h);
|
|
gui_state.lock().set_screen_width(w);
|
|
TuiTestSetup {
|
|
app_data,
|
|
gui_state,
|
|
fd,
|
|
area,
|
|
terminal,
|
|
ids,
|
|
}
|
|
}
|
|
|
|
/// Just a shorthand for when enumerating over result cells
|
|
pub fn get_result(
|
|
setup: &'_ TuiTestSetup,
|
|
) -> std::iter::Enumerate<std::slice::Chunks<'_, ratatui::buffer::Cell>> {
|
|
setup
|
|
.terminal
|
|
.backend()
|
|
.buffer()
|
|
.content
|
|
.chunks(usize::from(setup.area.width))
|
|
.enumerate()
|
|
}
|
|
|
|
/// Insert some logs into the first container
|
|
pub fn insert_logs(setup: &TuiTestSetup) {
|
|
let logs = (1..=3).map(|i| format!("{i} line {i}")).collect::<Vec<_>>();
|
|
setup.app_data.lock().update_log_by_id(logs, &setup.ids[0]);
|
|
}
|
|
|
|
#[allow(clippy::cast_precision_loss)]
|
|
// Add fixed data to the cpu & mem vecdeques
|
|
pub fn insert_all_chart_data(setup: &TuiTestSetup) {
|
|
for i in 1..=10 {
|
|
setup.app_data.lock().update_stats_by_id(
|
|
&setup.ids[0],
|
|
Some(i as f64),
|
|
Some(i * 10000),
|
|
i * 10000,
|
|
i,
|
|
i,
|
|
);
|
|
}
|
|
for i in 1..=3 {
|
|
setup.app_data.lock().update_stats_by_id(
|
|
&setup.ids[0],
|
|
Some(i as f64),
|
|
Some(i * 10000),
|
|
i * 10000,
|
|
i,
|
|
i,
|
|
);
|
|
}
|
|
}
|
|
|
|
// *************** //
|
|
// The whole layout //
|
|
// **************** //
|
|
#[test]
|
|
/// Check that the whole layout is drawn correctly
|
|
fn test_draw_blocks_whole_layout() {
|
|
let mut setup = test_setup(160, 30, true, true);
|
|
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::too_many_lines)]
|
|
/// Check that the whole layout is drawn correctly
|
|
fn test_draw_blocks_whole_layout_with_filter_bar() {
|
|
let mut setup = test_setup(160, 30, true, true);
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
|
|
setup.app_data.lock().containers.items[1]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
setup
|
|
.gui_state
|
|
.lock()
|
|
.status_push(crate::ui::Status::Filter);
|
|
setup.app_data.lock().filter_term_push('r');
|
|
setup.app_data.lock().filter_term_push('_');
|
|
setup.app_data.lock().filter_term_push('1');
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
|
|
#[test]
|
|
/// Check that the whole layout is drawn correctly when have long container name and long image name
|
|
fn test_draw_blocks_whole_layout_long_name() {
|
|
let mut setup = test_setup(190, 30, true, true);
|
|
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
|
|
setup.app_data.lock().containers.items[0].name =
|
|
ContainerName::from("a_long_container_name_for_the_purposes_of_this_test");
|
|
setup.app_data.lock().containers.items[0].image =
|
|
ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test");
|
|
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
|
|
#[test]
|
|
/// Check that the whole layout is drawn correctly when the logs panel is removed
|
|
fn test_draw_blocks_whole_layout_no_logs() {
|
|
let mut setup = test_setup(160, 30, true, true);
|
|
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
setup.gui_state.lock().log_height_zero();
|
|
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
|
|
#[test]
|
|
/// Check that the whole layout is drawn correctly when the logs panel height is ~4
|
|
fn test_draw_blocks_whole_layout_short_height_logs() {
|
|
let mut setup = test_setup(160, 30, true, true);
|
|
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
setup.gui_state.lock().log_height_zero();
|
|
|
|
for _ in 0..=3 {
|
|
setup.gui_state.lock().log_height_increase();
|
|
}
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
|
|
#[test]
|
|
/// Check that the whole layout is drawn with the help panel visible
|
|
fn test_draw_blocks_whole_layout_help_panel() {
|
|
let mut setup = test_setup(160, 40, true, true);
|
|
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
|
|
setup.gui_state.lock().status_push(Status::Help);
|
|
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
|
|
#[test]
|
|
/// Check that the whole layout is drawn with the error box is visible
|
|
fn test_draw_blocks_whole_layout_error() {
|
|
let mut setup = test_setup(160, 40, true, true);
|
|
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
|
|
setup.app_data.lock().set_error(
|
|
AppError::DockerCommand(crate::app_data::DockerCommand::Pause),
|
|
&setup.gui_state,
|
|
Status::Error,
|
|
);
|
|
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
|
|
#[test]
|
|
/// Check that the whole layout is drawn with the delete box is visible
|
|
fn test_draw_blocks_whole_layout_delete() {
|
|
let mut setup = test_setup(160, 40, true, true);
|
|
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
setup
|
|
.gui_state
|
|
.lock()
|
|
.set_delete_container(setup.app_data.lock().get_selected_container_id());
|
|
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
|
|
#[test]
|
|
/// Check that the whole layout is drawn with the info box is visible
|
|
fn test_draw_blocks_whole_layout_info_box() {
|
|
let mut setup = test_setup(160, 40, true, true);
|
|
|
|
insert_all_chart_data(&setup);
|
|
insert_logs(&setup);
|
|
setup.app_data.lock().containers.items[0]
|
|
.ports
|
|
.push(ContainerPorts {
|
|
ip: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
|
|
private: 8003,
|
|
public: Some(8003),
|
|
});
|
|
let colors = setup.app_data.lock().config.app_colors;
|
|
let keymap = setup.app_data.lock().config.keymap.clone();
|
|
setup.gui_state.lock().set_info_box("This is a test");
|
|
let fd = FrameData::from((&setup.app_data, &setup.gui_state));
|
|
setup
|
|
.terminal
|
|
.draw(|f| {
|
|
draw_frame(&setup.app_data, colors, &keymap, f, &fd, &setup.gui_state);
|
|
})
|
|
.unwrap();
|
|
|
|
assert_snapshot!(setup.terminal.backend());
|
|
}
|
|
}
|