feat: Added ports section, closes #21
This commit is contained in:
@@ -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
@@ -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 //
|
||||||
// ************** //
|
// ************** //
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
+278
-15
@@ -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,17 +2739,228 @@ 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 ",
|
||||||
@@ -2723,13 +2986,13 @@ mod tests {
|
|||||||
"│ │",
|
"│ │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"│ │",
|
"│ │",
|
||||||
"│ │",
|
|
||||||
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
"╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯",
|
||||||
"╭───────────────────────────────── cpu 03.00% ─────────────────────────────────╮╭────────────────────────────── memory 30.00 kB ───────────────────────────────╮",
|
"╭───────────────────────── cpu 03.00% ──────────────────────────╮╭─────────────────────── memory 30.00 kB ───────────────────────╮╭────────── ports ───────────╮",
|
||||||
"│10.00%│ •••••• ││100.00 kB│ •••••• │",
|
"│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│",
|
||||||
"│ │••••• •••• ││ │••••• ••• │",
|
"│ │ ••• • ││ │ ••• • ││ 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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user