Merge branch 'tests/init' into dev

This commit is contained in:
Jack Wills
2024-01-16 10:39:33 +00:00
12 changed files with 3455 additions and 245 deletions
+2 -1
View File
@@ -157,7 +157,8 @@ see <a href="https://forums.raspberrypi.com/viewtopic.php?t=203128" target='_bla
## Tests ## Tests
As of yet untested, needs work ~~As of yet untested, needs work~~
The work has been done
```shell ```shell
cargo test -- --test-threads=1 cargo test -- --test-threads=1
+3 -3
View File
@@ -215,19 +215,19 @@ check_typos() {
} }
# Make sure the unused lint isn't used # Make sure the unused lint isn't used
check_allow_unsued() { check_allow_unused() {
matches_any=$(find . -type d \( -name .git -o -name target \) -prune -o -type f -exec grep -lE '^#!\[allow\(unused\)\]$' {} +) matches_any=$(find . -type d \( -name .git -o -name target \) -prune -o -type f -exec grep -lE '^#!\[allow\(unused\)\]$' {} +)
matches_cargo=$(grep "^unused = \"allow\"" ./Cargo.toml) matches_cargo=$(grep "^unused = \"allow\"" ./Cargo.toml)
if [ -n "$matches_any" ]; then if [ -n "$matches_any" ]; then
error_close "\"#[allow(unused)]\" in ${matches_any}" error_close "\"#[allow(unused)]\" in ${matches_any}"
elif [ -n "$matches_cargo" ]; then elif [ -n "$matches_cargo" ]; then
error_close "\"unsed = \"allow\"\" in Cargo.toml" error_close "\"unused = \"allow\"\" in Cargo.toml"
fi fi
} }
# Full flow to create a new release # Full flow to create a new release
release_flow() { release_flow() {
check_allow_unsued check_allow_unused
check_typos check_typos
check_git check_git
+5 -5
View File
@@ -3,9 +3,11 @@ networks:
oxker-example-net: oxker-example-net:
name: oxker-examaple-net name: oxker-examaple-net
services: services:
postgres: postgres_but_with_a_longer_container_name:
image: postgres:alpine3.19 container_name: postgres_but_with_a_longer_container_name
container_name: postgres build:
dockerfile: ./postgres.Dockerfile
context: "."
environment: environment:
- POSTGRES_PASSWORD=never_use_this_password_in_production - POSTGRES_PASSWORD=never_use_this_password_in_production
ipc: private ipc: private
@@ -39,5 +41,3 @@ services:
resources: resources:
limits: limits:
memory: 512M memory: 512M
+1
View File
@@ -0,0 +1 @@
FROM postgres:16-alpine3.19
+108 -6
View File
@@ -60,6 +60,13 @@ macro_rules! unit_struct {
} }
} }
#[cfg(test)]
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}
impl$name { impl$name {
pub fn get(&self) -> &str { pub fn get(&self) -> &str {
self.0.as_str() self.0.as_str()
@@ -93,7 +100,7 @@ macro_rules! unit_struct {
unit_struct!(ContainerName); unit_struct!(ContainerName);
unit_struct!(ContainerImage); unit_struct!(ContainerImage);
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct StatefulList<T> { pub struct StatefulList<T> {
pub state: ListState, pub state: ListState,
pub items: Vec<T>, pub items: Vec<T>,
@@ -154,7 +161,7 @@ impl<T> StatefulList<T> {
.state .state
.selected() .selected()
.map_or(0, |value| if len > 0 { value + 1 } else { value }); .map_or(0, |value| if len > 0 { value + 1 } else { value });
format!("{c}/{}", self.items.len()) format!(" {c}/{}", self.items.len())
} }
} }
} }
@@ -234,7 +241,7 @@ impl fmt::Display for State {
} }
/// Items for the container control list /// Items for the container control list
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DockerControls { pub enum DockerControls {
Pause, Pause,
Restart, Restart,
@@ -416,7 +423,7 @@ impl fmt::Display for LogsTz {
/// Store the logs alongside a HashSet, each log *should* generate a unique timestamp, /// Store the logs alongside a HashSet, each log *should* generate a unique timestamp,
/// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the /// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the
/// stateful list dependent on whethere the timestamp is in the HashSet or not /// stateful list dependent on whethere the timestamp is in the HashSet or not
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Logs { pub struct Logs {
logs: StatefulList<ListItem<'static>>, logs: StatefulList<ListItem<'static>>,
tz: HashSet<LogsTz>, tz: HashSet<LogsTz>,
@@ -475,7 +482,7 @@ impl Logs {
} }
/// Info for each container /// Info for each container
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerItem { pub struct ContainerItem {
pub created: u64, pub created: u64,
pub cpu_stats: VecDeque<CpuStats>, pub cpu_stats: VecDeque<CpuStats>,
@@ -594,7 +601,7 @@ impl ContainerItem {
} }
/// Container information panel headings + widths, for nice pretty formatting /// Container information panel headings + widths, for nice pretty formatting
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Columns { pub struct Columns {
pub name: (Header, u8), pub name: (Header, u8),
pub state: (Header, u8), pub state: (Header, u8),
@@ -623,3 +630,98 @@ impl Columns {
} }
} }
} }
#[cfg(test)]
mod tests {
use ratatui::widgets::ListItem;
use crate::{
app_data::{ContainerImage, Logs},
ui::log_sanitizer,
};
use super::{ByteStats, ContainerName, CpuStats, LogsTz};
#[test]
// Display CpuStats as a string
fn test_container_state_cpustats_to_string() {
let test = |f: f64, s: &str| {
assert_eq!(CpuStats::new(f).to_string(), s);
};
test(0.0, "00.00%");
test(1.5, "01.50%");
test(15.15, "15.15%");
test(150.15, "150.15%");
}
#[test]
// Display bytestats as a string, convert into correct data unit (Kb, MB, GB)
fn test_container_state_bytestats_to_string() {
let test = |u: u64, s: &str| {
assert_eq!(ByteStats::new(u).to_string(), s);
};
test(0, "0.00 kB");
test(150, "0.15 kB");
test(1500, "1.50 kB");
test(150_000, "150.00 kB");
test(1_500_000, "1.50 MB");
test(15_000_000, "15.00 MB");
test(150_000_000, "150.00 MB");
test(1_500_000_000, "1.50 GB");
test(15_000_000_000, "15.00 GB");
test(150_000_000_000, "150.00 GB");
}
#[test]
/// ContainerName as string truncated correctly
fn test_container_state_container_name_to_string() {
let result = ContainerName::from("name_01");
assert_eq!(result.to_string(), "name_01");
let result = ContainerName::from("name_01_name_01_name_01_name_01_");
assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…");
let result = result.get();
assert_eq!(result, "name_01_name_01_name_01_name_01_");
}
#[test]
/// ContainerImage as string truncated correctly
fn test_container_state_container_image() {
let result = ContainerImage::from("name_01");
assert_eq!(result.to_string(), "name_01");
let result = ContainerImage::from("name_01_name_01_name_01_name_01_");
assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…");
let result = result.get();
assert_eq!(result, "name_01_name_01_name_01_name_01_");
}
#[test]
/// Logs can only contain 1 entry per LogzTz
fn test_container_state_logz() {
let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet";
let tz = LogsTz::from(input);
let mut logs = Logs::default();
let line = log_sanitizer::remove_ansi(input);
logs.insert(ListItem::new(line.clone()), tz.clone());
logs.insert(ListItem::new(line.clone()), tz.clone());
logs.insert(ListItem::new(line), tz);
assert_eq!(logs.logs.items.len(), 1);
let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet";
let tz = LogsTz::from(input);
let line = log_sanitizer::remove_ansi(input);
logs.insert(ListItem::new(line.clone()), tz.clone());
logs.insert(ListItem::new(line.clone()), tz.clone());
logs.insert(ListItem::new(line), tz);
assert_eq!(logs.logs.items.len(), 2);
}
}
+1197 -147
View File
File diff suppressed because it is too large Load Diff
+146 -4
View File
@@ -71,6 +71,7 @@ pub struct DockerData {
impl DockerData { impl DockerData {
/// Use docker stats to calculate current cpu usage /// Use docker stats to calculate current cpu usage
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
// FIX: this can overflow
fn calculate_usage(stats: &Stats) -> f64 { fn calculate_usage(stats: &Stats) -> f64 {
let mut cpu_percentage = 0.0; let mut cpu_percentage = 0.0;
let previous_cpu = stats.precpu_stats.cpu_usage.total_usage; let previous_cpu = stats.precpu_stats.cpu_usage.total_usage;
@@ -150,7 +151,7 @@ impl DockerData {
app_data app_data
.lock() .lock()
.update_stats(&id, cpu_stats, mem_stat, mem_limit, rx, tx); .update_stats_by_id(&id, cpu_stats, mem_stat, mem_limit, rx, tx);
} }
} }
spawns.lock().remove(&spawn_id); spawns.lock().remove(&spawn_id);
@@ -162,7 +163,6 @@ impl DockerData {
/// Update all stats, spawn each container into own tokio::spawn thread /// Update all stats, spawn each container into own tokio::spawn thread
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) { fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
for (state, id) in all_ids { for (state, id) in all_ids {
// let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len())));
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);
let spawns = Arc::clone(&self.spawns); let spawns = Arc::clone(&self.spawns);
@@ -436,7 +436,7 @@ impl DockerData {
} }
/// Send an update message every x ms, where x is the args.docker_interval /// Send an update message every x ms, where x is the args.docker_interval
fn croner(args: &CliArgs, docker_tx: Sender<DockerMessage>) { fn scheduler(args: &CliArgs, docker_tx: Sender<DockerMessage>) {
let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval)); let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
let mut now = std::time::Instant::now(); let mut now = std::time::Instant::now();
tokio::spawn(async move { tokio::spawn(async move {
@@ -472,10 +472,152 @@ impl DockerData {
spawns: Arc::new(Mutex::new(HashMap::new())), spawns: Arc::new(Mutex::new(HashMap::new())),
}; };
inner.initialise_container_data().await; inner.initialise_container_data().await;
Self::croner(&args, docker_tx); Self::scheduler(&args, docker_tx);
inner.message_handler().await; inner.message_handler().await;
} }
} }
} }
// tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers // tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers
#[cfg(test)]
mod tests {
use bollard::container::{
BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData,
};
use super::*;
#[allow(clippy::too_many_lines)]
fn gen_stats(x: u64, y: u64) -> Stats {
Stats {
read: String::new(),
preread: String::new(),
num_procs: 0,
pids_stats: PidsStats {
current: None,
limit: None,
},
network: None,
networks: None,
memory_stats: MemoryStats {
stats: None,
max_usage: None,
usage: None,
failcnt: None,
limit: None,
commit: None,
commit_peak: None,
commitbytes: None,
commitpeakbytes: None,
privateworkingset: None,
},
blkio_stats: BlkioStats {
io_service_bytes_recursive: None,
io_serviced_recursive: None,
io_queue_recursive: None,
io_service_time_recursive: None,
io_wait_time_recursive: None,
io_merged_recursive: None,
io_time_recursive: None,
sectors_recursive: None,
},
cpu_stats: CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![
291_593_800,
182_192_900,
195_048_700,
23_032_300,
132_928_700,
235_555_600,
120_225_700,
175_752_000,
213_060_300,
95_321_600,
226_821_000,
0,
109_151_300,
0,
86_240_200,
1_884_400,
59_077_300,
23_224_900,
95_386_300,
144_987_400,
]),
total_usage: 250_000_000,
usage_in_usermode: 1_020_000_000,
usage_in_kernelmode: 1_030_000_000,
},
system_cpu_usage: Some(x),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
},
precpu_stats: CPUStats {
cpu_usage: CPUUsage {
percpu_usage: Some(vec![
291_593_800,
182_192_900,
195_048_700,
23_032_300,
132_928_700,
235_555_600,
120_225_700,
175_752_000,
213_060_300,
95_321_600,
226_821_000,
0,
109_151_300,
0,
86_240_200,
1_884_400,
59_077_300,
23_224_900,
93_831_100,
144_987_400,
]),
total_usage: 200_000_000,
usage_in_usermode: 1_020_000_000,
usage_in_kernelmode: 1_020_000_000,
},
system_cpu_usage: Some(y),
online_cpus: Some(1),
throttling_data: ThrottlingData {
periods: 0,
throttled_periods: 0,
throttled_time: 0,
},
},
storage_stats: StorageStats {
read_count_normalized: None,
read_size_bytes: None,
write_count_normalized: None,
write_size_bytes: None,
},
name: "/container_1".to_owned(),
id: "1".to_owned(),
}
}
#[test]
#[allow(clippy::float_cmp)]
/// Test the stats calculator, had to cheat here to get round input/outputs
fn test_calculate_usage_no_previous_cpu() {
let stats = gen_stats(1_000_000_000, 900_000_000);
let result = DockerData::calculate_usage(&stats);
assert_eq!(result, 50.0);
let stats = gen_stats(1_000_000_000, 800_000_000);
let result = DockerData::calculate_usage(&stats);
assert_eq!(result, 25.0);
let stats = gen_stats(1_000_000_000, 750_000_000);
let result = DockerData::calculate_usage(&stats);
assert_eq!(result, 20.00);
}
}
+6 -6
View File
@@ -268,11 +268,11 @@ impl InputHandler {
// 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 {
let option_command = self.app_data.lock().selected_docker_command(); let option_command = self.app_data.lock().selected_docker_controls();
if let Some(command) = option_command { if let Some(command) = option_command {
// Poor way of disallowing commands to be sent to a containerised okxer // Poor way of disallowing commands to be sent to a containerised okxer
if self.app_data.lock().is_oxker() { if self.app_data.lock().is_oxker_in_container() {
return; return;
}; };
let option_id = self.app_data.lock().get_selected_container_id(); let option_id = self.app_data.lock().get_selected_container_id();
@@ -337,7 +337,7 @@ impl InputHandler {
match selected_panel { match selected_panel {
SelectablePanel::Containers => locked_data.containers_start(), SelectablePanel::Containers => locked_data.containers_start(),
SelectablePanel::Logs => locked_data.log_start(), SelectablePanel::Logs => locked_data.log_start(),
SelectablePanel::Commands => locked_data.docker_command_start(), SelectablePanel::Commands => locked_data.docker_controls_start(),
} }
} }
@@ -348,7 +348,7 @@ impl InputHandler {
match selected_panel { match selected_panel {
SelectablePanel::Containers => locked_data.containers_end(), SelectablePanel::Containers => locked_data.containers_end(),
SelectablePanel::Logs => locked_data.log_end(), SelectablePanel::Logs => locked_data.log_end(),
SelectablePanel::Commands => locked_data.docker_command_end(), SelectablePanel::Commands => locked_data.docker_controls_end(),
} }
} }
@@ -481,7 +481,7 @@ impl InputHandler {
match selected_panel { match selected_panel {
SelectablePanel::Containers => locked_data.containers_next(), SelectablePanel::Containers => locked_data.containers_next(),
SelectablePanel::Logs => locked_data.log_next(), SelectablePanel::Logs => locked_data.log_next(),
SelectablePanel::Commands => locked_data.docker_command_next(), SelectablePanel::Commands => locked_data.docker_controls_next(),
}; };
} }
@@ -492,7 +492,7 @@ impl InputHandler {
match selected_panel { match selected_panel {
SelectablePanel::Containers => locked_data.containers_previous(), SelectablePanel::Containers => locked_data.containers_previous(),
SelectablePanel::Logs => locked_data.log_previous(), SelectablePanel::Logs => locked_data.log_previous(),
SelectablePanel::Commands => locked_data.docker_command_previous(), SelectablePanel::Commands => locked_data.docker_controls_previous(),
} }
} }
} }
+79
View File
@@ -164,3 +164,82 @@ async fn main() {
} }
} }
} }
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)]
mod tests {
use bollard::service::ContainerSummary;
use crate::{
app_data::{AppData, ContainerId, ContainerItem, State, StatefulList},
parse_args::CliArgs,
};
pub const fn gen_args() -> CliArgs {
CliArgs {
color: false,
docker_interval: 1000,
gui: true,
host: None,
in_container: false,
save_dir: None,
raw: false,
show_self: false,
timestamp: false,
use_cli: false,
}
}
pub fn gen_item(id: &ContainerId, index: usize) -> ContainerItem {
ContainerItem::new(
u64::try_from(index).unwrap(),
id.clone(),
format!("image_{index}"),
false,
format!("container_{index}"),
State::Running,
format!("Up {index} hour"),
)
}
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
AppData {
containers: StatefulList::new(containers.to_vec()),
error: None,
sorted_by: None,
args: gen_args(),
}
}
pub fn gen_containers() -> (Vec<ContainerId>, Vec<ContainerItem>) {
let ids = (1..=3)
.map(|i| ContainerId::from(format!("{i}").as_str()))
.collect::<Vec<_>>();
let containers = ids
.iter()
.enumerate()
.map(|(index, id)| gen_item(id, index + 1))
.collect::<Vec<_>>();
(ids, containers)
}
pub fn gen_container_summary(index: usize, state: &str) -> ContainerSummary {
ContainerSummary {
id: Some(format!("{index}")),
names: Some(vec![format!("container_{}", index)]),
image: Some(format!("image_{index}")),
image_id: Some(format!("{index}")),
command: None,
created: Some(i64::try_from(index).unwrap()),
ports: None,
size_rw: None,
size_root_fs: None,
labels: None,
state: Some(state.to_owned()),
status: Some(format!("Up {index} hour")),
host_config: None,
network_settings: None,
mounts: None,
}
}
}
+76
View File
@@ -72,3 +72,79 @@ pub mod log_sanitizer {
} }
} }
} }
#[cfg(test)]
mod tests {
use ratatui::{
style::{Color, Style},
text::{Line, Span},
};
use super::log_sanitizer;
// This spells out "oxker", with each char having a foreground and background colour
const INPUT: &str = "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m";
#[test]
/// Return test raw, as in show escape codes
fn color_match_raw() {
let result = log_sanitizer::raw(INPUT);
let expected = vec![Line {
spans: [Span {
content: std::borrow::Cow::Borrowed(
"\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m",
),
style: Style::default(),
}]
.to_vec(),
alignment: None,
}];
assert_eq!(result, expected);
}
#[test]
// Use the escape codes to colorize the text
fn color_match_colorize() {
let result = log_sanitizer::colorize_logs(INPUT);
let expected = vec![Line {
spans: vec![
Span {
content: std::borrow::Cow::Borrowed("o"),
style: Style::default().fg(Color::Red).bg(Color::White),
},
Span {
content: std::borrow::Cow::Borrowed("x"),
style: Style::default().fg(Color::Green).bg(Color::Black),
},
Span {
content: std::borrow::Cow::Borrowed("k"),
style: Style::default().fg(Color::Yellow).bg(Color::Red),
},
Span {
content: std::borrow::Cow::Borrowed("e"),
style: Style::default().fg(Color::Blue).bg(Color::Green),
},
Span {
content: std::borrow::Cow::Borrowed("r"),
style: Style::default().fg(Color::Magenta).bg(Color::Yellow),
},
],
alignment: None,
}];
assert_eq!(result, expected);
}
#[test]
// Remove all escape ansi codes from given input
fn color_match_remove_ansi() {
let result = log_sanitizer::remove_ansi(INPUT);
let expected = vec![Line {
spans: vec![Span {
content: std::borrow::Cow::Borrowed("oxker"),
style: Style::default(),
}],
alignment: None,
}];
assert_eq!(result, expected);
}
}
+1821 -41
View File
File diff suppressed because it is too large Load Diff
+7 -28
View File
@@ -217,22 +217,6 @@ impl Ui {
} }
} }
#[cfg(not(debug_assertions))]
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
.split(f.size())
}
#[cfg(debug_assertions)]
fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Min(1), Constraint::Min(100)].as_ref())
.split(f.size())
}
/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here /// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here
#[derive(Debug)] #[derive(Debug)]
pub struct FrameData { pub struct FrameData {
@@ -279,21 +263,16 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
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 whole_layout = get_wholelayout(f); let whole_layout = Layout::default()
#[cfg(debug_assertions)] .direction(Direction::Vertical)
draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string()); .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
.split(f.size());
#[cfg(debug_assertions)]
let whole_layout_split = (1, 2);
#[cfg(not(debug_assertions))]
let whole_layout_split = (0, 1);
// Split into 3, containers+controls, logs, then graphs // Split into 3, containers+controls, logs, then graphs
let upper_main = Layout::default() let upper_main = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref())
.split(whole_layout[whole_layout_split.1]); .split(whole_layout[1]);
let top_split = if fd.has_containers { let top_split = if fd.has_containers {
vec![Constraint::Percentage(90), Constraint::Percentage(10)] vec![Constraint::Percentage(90), Constraint::Percentage(10)]
@@ -318,11 +297,11 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
.constraints(lower_split) .constraints(lower_split)
.split(upper_main[1]); .split(upper_main[1]);
draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state, &fd.columns); draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state);
draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state); draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state);
draw_blocks::heading_bar(whole_layout[whole_layout_split.0], f, &fd, gui_state); draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state);
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(