wip: sort by

This commit is contained in:
Jack Wills
2022-07-22 16:33:40 +00:00
parent dc4a62910c
commit d14744b378
6 changed files with 258 additions and 166 deletions
+34 -31
View File
@@ -5,9 +5,12 @@ use tui::{
widgets::{ListItem, ListState}, widgets::{ListItem, ListState},
}; };
use super::Header;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StatefulList<T> { pub struct StatefulList<T> {
pub state: ListState, pub state: ListState,
// HASH MAP!
pub items: Vec<T>, pub items: Vec<T>,
} }
@@ -114,18 +117,18 @@ impl State {
_ => Color::Red, _ => Color::Red,
} }
} }
pub fn as_text(&self) -> &'static str { pub fn as_text(&self) -> &'static str {
match self { match self {
Self::Dead => "dead", Self::Dead => "dead",
Self::Exited => "exited", Self::Exited => "exited",
Self::Paused => "paused", Self::Paused => "paused",
Self::Removing => "removing", Self::Removing => "removing",
Self::Restarting => "restarting", Self::Restarting => "restarting",
Self::Running => "running", Self::Running => "running",
Self::Unknown => "unknown", Self::Unknown => "unknown",
} }
} }
} }
impl From<&str> for State { impl From<&str> for State {
fn from(input: &str) -> Self { fn from(input: &str) -> Self {
@@ -420,31 +423,31 @@ impl ContainerItem {
/// Container information panel headings + widths, for nice pretty formatting /// Container information panel headings + widths, for nice pretty formatting
#[derive(Debug)] #[derive(Debug)]
pub struct Columns { pub struct Columns {
pub state: (String, usize), pub state: (Header, usize),
pub status: (String, usize), pub status: (Header, usize),
pub cpu: (String, usize), pub cpu: (Header, usize),
pub mem: (String, usize), pub mem: (Header, usize),
pub id: (String, usize), pub id: (Header, usize),
pub name: (String, usize), pub name: (Header, usize),
pub image: (String, usize), pub image: (Header, usize),
pub net_rx: (String, usize), pub net_rx: (Header, usize),
pub net_tx: (String, usize), pub net_tx: (Header, usize),
} }
impl Columns { impl Columns {
//. (Column titles, minimum header string length) // (Column titles, minimum header string length)
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
state: (String::from("state"), 11), state: (Header::State, 11),
status: (String::from("status"), 16), status: (Header::Status, 16),
// 7 to allow for "100.00%" // 7 to allow for "100.00%"
cpu: (String::from("cpu"), 7), cpu: (Header::Cpu, 7),
mem: (String::from("memory/limit"), 12), mem: (Header::Memory, 12),
id: (String::from("id"), 8), id: (Header::Id, 8),
name: (String::from("name"), 4), name: (Header::Name, 4),
image: (String::from("image"), 5), image: (Header::Image, 5),
net_rx: (String::from("↓ rx"), 5), net_rx: (Header::Rx, 5),
net_tx: (String::from("↑ tx"), 5), net_tx: (Header::Tx, 5),
} }
} }
} }
+136 -96
View File
@@ -1,4 +1,5 @@
use bollard::models::ContainerSummary; use bollard::models::ContainerSummary;
use core::fmt;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use tui::widgets::ListItem; use tui::widgets::ListItem;
@@ -16,31 +17,58 @@ pub struct AppData {
pub containers: StatefulList<ContainerItem>, pub containers: StatefulList<ContainerItem>,
pub init: bool, pub init: bool,
pub show_error: bool, pub show_error: bool,
// todo sorted_by: Option<(Header, SortedOrder)>,
sort_by: Header
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum SortedOrder{ pub enum SortedOrder {
Asc, Asc,
Desc Desc,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum Header{ pub enum Header {
State, State,
Status, Status,
Cpu, Cpu,
Memory, Memory,
Id, Id,
Name, Name,
Image, Image,
Rx, Rx,
Tx Tx,
} }
/// Convert errors into strings to display
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let disp = match self {
Self::State => "state",
Self::Status => "status",
Self::Cpu => "cpu",
Self::Memory => "memory/limit",
Self::Id => "id",
Self::Name => "name",
Self::Image => "image",
Self::Rx => "↓ rx",
Self::Tx => "↑ tx",
};
write!(f, "{:>x$}", disp, x = f.width().unwrap_or(1))
}
}
impl AppData { impl AppData {
pub fn get_sorted(&self) -> Option<(Header, SortedOrder)> {
self.sorted_by.clone()
}
/// Change the sorted order, also set the selected pointer!
pub fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) {
self.sorted_by = x;
let id = self.get_selected_container_id();
self.sort_containers();
self.containers.state.select(self.containers.items.iter().position(|i| Some(i.id.to_owned()) == id));
}
/// Generate a default app_state /// Generate a default app_state
pub fn default(args: CliArgs) -> Self { pub fn default(args: CliArgs) -> Self {
Self { Self {
@@ -50,7 +78,7 @@ impl AppData {
init: false, init: false,
logs_parsed: false, logs_parsed: false,
show_error: false, show_error: false,
sort_by: Header::Memory, sorted_by: Some((Header::Memory, SortedOrder::Asc)),
} }
} }
@@ -141,85 +169,97 @@ impl AppData {
output output
} }
/// Sort the containers vec, based on a heading, either ascending or descending
pub fn sort_containers(&mut self, so: SortedOrder) { pub fn sort_containers(&mut self) {
if let Some((head, so)) = self.sorted_by.as_ref() {
// State, match head {
// Status, Header::State => match so {
// Cpu, SortedOrder::Asc => self
// Memory, .containers
// Id, .items
// Name, .sort_by(|a, b| a.state.as_text().cmp(b.state.as_text())),
// Image, SortedOrder::Desc => self
// Rx, .containers
// Tx .items
match self.sort_by { .sort_by(|a, b| b.state.as_text().cmp(a.state.as_text())),
Header::State => { },
match so { Header::Status => match so {
SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.state.as_text().cmp(b.state.as_text())), SortedOrder::Asc => self
SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.state.as_text().cmp(a.state.as_text())), .containers
} .items
.sort_by(|a, b| a.status.cmp(&b.status)),
}, SortedOrder::Desc => self
Header::Status => { .containers
match so { .items
SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.status.cmp(&b.status)), .sort_by(|a, b| b.status.cmp(&a.status)),
SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.status.cmp(&a.status)), },
} Header::Status => match so {
}, SortedOrder::Asc => self
.containers
Header::Status => { .items
match so { .sort_by(|a, b| a.status.cmp(&b.status)),
SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.status.cmp(&b.status)), SortedOrder::Desc => self
SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.status.cmp(&a.status)), .containers
} .items
}, .sort_by(|a, b| b.status.cmp(&a.status)),
},
Header::Cpu => { Header::Cpu => match so {
match so { SortedOrder::Desc => self
SortedOrder::Desc => .containers
self.containers.items.sort_by(|a,b|a.cpu_stats.back().cmp(&b.cpu_stats.back())), .items
SortedOrder::Asc => self.containers.items.sort_by(|a,b|b.cpu_stats.back().cmp(&a.cpu_stats.back())) .sort_by(|a, b| a.cpu_stats.back().cmp(&b.cpu_stats.back())),
} SortedOrder::Asc => self
}, .containers
.items
Header::Memory => { .sort_by(|a, b| b.cpu_stats.back().cmp(&a.cpu_stats.back())),
match so { },
SortedOrder::Desc => Header::Memory => match so {
self.containers.items.sort_by(|a,b|a.mem_stats.back().cmp(&b.mem_stats.back())), SortedOrder::Desc => self
SortedOrder::Asc => self.containers.items.sort_by(|a,b|b.mem_stats.back().cmp(&a.mem_stats.back())) .containers
} .items
}, .sort_by(|a, b| a.mem_stats.back().cmp(&b.mem_stats.back())),
Header::Image => { SortedOrder::Asc => self
match so { .containers
SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.image.cmp(&b.image)), .items
SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.image.cmp(&a.image)), .sort_by(|a, b| b.mem_stats.back().cmp(&a.mem_stats.back())),
} },
}, Header::Id => match so {
Header::Name => { SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.id.cmp(&b.id)),
match so { SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.id.cmp(&a.id)),
SortedOrder::Asc => self.containers.items.sort_by(|a,b|a.name.cmp(&b.name)), },
SortedOrder::Desc => self.containers.items.sort_by(|a,b|b.name.cmp(&a.name)), Header::Image => match so {
} SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.image.cmp(&b.image)),
}, SortedOrder::Desc => {
_ => () self.containers.items.sort_by(|a, b| b.image.cmp(&a.image))
} }
} },
Header::Name => match so {
SortedOrder::Asc => self.containers.items.sort_by(|a, b| a.name.cmp(&b.name)),
SortedOrder::Desc => self.containers.items.sort_by(|a, b| b.name.cmp(&a.name)),
// match so { },
// SortedOrder::Asc => self.containers.items.sort_by(|a,b|b.name.cmp(&a.name)), Header::Rx => match so {
// SortedOrder::Desc => self.containers.items.sort_by(|a,b|a.name.cmp(&b.name)) SortedOrder::Asc => self
// } .containers
// } .items
.sort_by(|a, b| a.net_rx.cmp(&b.net_rx)),
pub fn sort_by_id(&mut self, so: SortedOrder) { SortedOrder::Desc => self
match so { .containers
SortedOrder::Asc => self.containers.items.sort_by(|a,b|b.id.cmp(&a.id)), .items
SortedOrder::Desc => self.containers.items.sort_by(|a,b|a.id.cmp(&b.id)) .sort_by(|a, b| b.net_rx.cmp(&a.net_rx)),
} },
} Header::Tx => match so {
SortedOrder::Asc => self
.containers
.items
.sort_by(|a, b| a.net_tx.cmp(&b.net_tx)),
SortedOrder::Desc => self
.containers
.items
.sort_by(|a, b| b.net_tx.cmp(&a.net_tx)),
},
}
}
}
/// Find the index of the currently selected single log line /// Find the index of the currently selected single log line
pub fn get_selected_log_index(&self) -> Option<usize> { pub fn get_selected_log_index(&self) -> Option<usize> {
+24 -25
View File
@@ -1,6 +1,7 @@
use bollard::{ use bollard::{
container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions}, container::{ListContainersOptions, LogsOptions, StartContainerOptions, Stats, StatsOptions},
Docker, models::ContainerSummary, models::ContainerSummary,
Docker,
}; };
use futures_util::{future::join_all, StreamExt}; use futures_util::{future::join_all, StreamExt};
use parking_lot::Mutex; use parking_lot::Mutex;
@@ -8,7 +9,7 @@ use std::sync::Arc;
use tokio::{sync::mpsc::Receiver, task::JoinHandle}; use tokio::{sync::mpsc::Receiver, task::JoinHandle};
use crate::{ use crate::{
app_data::{AppData, DockerControls, SortedOrder, Header}, app_data::{AppData, DockerControls, Header, SortedOrder},
app_error::AppError, app_error::AppError,
parse_args::CliArgs, parse_args::CliArgs,
ui::GuiState, ui::GuiState,
@@ -123,25 +124,25 @@ impl DockerData {
} }
} }
// pub fn sort_containers(i: &mut [ContainerSummary], so: SortedOrder, header: Header) -> &[ContainerSummary] { // pub fn sort_containers(i: &mut [ContainerSummary], so: SortedOrder, header: Header) -> &[ContainerSummary] {
// match header { // match header {
// Header::State => { // Header::State => {
// match so { // match so {
// SortedOrder::Asc => i.sort_by(|a,b|b.state.cmp(&a.state)), // SortedOrder::Asc => i.sort_by(|a,b|b.state.cmp(&a.state)),
// SortedOrder::Desc => i.sort_by(|a,b|a.state.cmp(&b.state)), // SortedOrder::Desc => i.sort_by(|a,b|a.state.cmp(&b.state)),
// } // }
// }, // },
// Header::Image => { // Header::Image => {
// match so { // match so {
// SortedOrder::Asc => i.sort_by(|a,b|b.image.cmp(&a.image)), // SortedOrder::Asc => i.sort_by(|a,b|b.image.cmp(&a.image)),
// SortedOrder::Desc => i.sort_by(|a,b|a.image.cmp(&b.image)), // SortedOrder::Desc => i.sort_by(|a,b|a.image.cmp(&b.image)),
// } // }
// }, // },
// _ => () // _ => ()
// } // }
// i // i
// } // }
/// Get all current containers, handle into ContainerItem in the app_data struct rather than here /// Get all current containers, handle into ContainerItem in the app_data struct rather than here
/// Just make sure that items sent are guaranteed to have an id /// Just make sure that items sent are guaranteed to have an id
@@ -163,13 +164,12 @@ impl DockerData {
.filter(|i| i.id.is_some()) .filter(|i| i.id.is_some())
.for_each(|c| output.push(c.to_owned())); .for_each(|c| output.push(c.to_owned()));
// containers.so
// containers.so // let a = Self::sort_containers(&mut output, SortedOrder::Asc, Header::State);
// let a = Self::sort_containers(&mut output, SortedOrder::Asc, Header::State);
self.app_data.lock().update_containers(&output); self.app_data.lock().update_containers(&output);
// self.app_data.lock().sort_containers(SortedOrder::Asc, Header::State); // self.app_data.lock().sort_containers(SortedOrder::Asc, Header::State);
output output
.iter() .iter()
@@ -241,7 +241,6 @@ impl DockerData {
}; };
self.update_all_container_stats(&all_ids).await; self.update_all_container_stats(&all_ids).await;
} }
/// Animate the loading icon /// Animate the loading icon
+24 -1
View File
@@ -18,7 +18,7 @@ use tui::layout::Rect;
mod message; mod message;
use crate::{ use crate::{
app_data::{AppData, DockerControls}, app_data::{AppData, DockerControls, Header, SortedOrder},
app_error::AppError, app_error::AppError,
docker_data::DockerMessage, docker_data::DockerMessage,
ui::{GuiState, SelectablePanel}, ui::{GuiState, SelectablePanel},
@@ -115,6 +115,20 @@ impl InputHandler {
self.mouse_capture = !self.mouse_capture; self.mouse_capture = !self.mouse_capture;
} }
fn sort(&self, header: Header) {
let mut locked_data = self.app_data.lock();
if let Some((s, h)) = locked_data.get_sorted().as_ref() {
match (s, h) {
(header, SortedOrder::Asc) => {
locked_data.set_sorted(Some((header.to_owned(), SortedOrder::Desc)))
}
_ => locked_data.set_sorted(Some((header, SortedOrder::Asc))),
}
} else {
locked_data.set_sorted(Some((header, SortedOrder::Asc)))
}
}
/// Handle any keyboard button events /// Handle any keyboard button events
async fn button_press(&mut self, key_code: KeyCode) { async fn button_press(&mut self, key_code: KeyCode) {
let show_error = self.app_data.lock().show_error; let show_error = self.app_data.lock().show_error;
@@ -140,6 +154,15 @@ impl InputHandler {
} }
} else { } else {
match key_code { match key_code {
KeyCode::Char('1') => self.sort(Header::State),
KeyCode::Char('2') => self.sort(Header::Status),
KeyCode::Char('3') => self.sort(Header::Cpu),
KeyCode::Char('4') => self.sort(Header::Memory),
KeyCode::Char('5') => self.sort(Header::Id),
KeyCode::Char('6') => self.sort(Header::Image),
KeyCode::Char('7') => self.sort(Header::Name),
KeyCode::Char('8') => self.sort(Header::Rx),
KeyCode::Char('9') => self.sort(Header::Tx),
KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst), KeyCode::Char('q') => self.is_running.store(false, Ordering::SeqCst),
KeyCode::Char('h') => self.gui_state.lock().show_help = true, KeyCode::Char('h') => self.gui_state.lock().show_help = true,
KeyCode::Char('m') => self.m_button(), KeyCode::Char('m') => self.m_button(),
+38 -13
View File
@@ -14,7 +14,7 @@ use tui::{
Frame, Frame,
}; };
use crate::app_data::{SortedOrder, Header}; use crate::app_data::{Header, SortedOrder};
use crate::{ use crate::{
app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats}, app_data::{AppData, ByteStats, Columns, CpuStats, State, Stats},
app_error::AppError, app_error::AppError,
@@ -124,9 +124,6 @@ pub fn draw_containers<B: Backend>(
widths: &Columns, widths: &Columns,
) { ) {
let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers); let block = generate_block(app_data, area, gui_state, SelectablePanel::Containers);
app_data.lock().sort_containers(SortedOrder::Asc);
let items = app_data let items = app_data
.lock() .lock()
.containers .containers
@@ -361,12 +358,39 @@ pub fn draw_heading_bar<B: Backend>(
has_containers: bool, has_containers: bool,
loading_icon: String, loading_icon: String,
info_visible: bool, info_visible: bool,
sorted_by: Option<(Header, SortedOrder)>,
) { ) {
let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black)); let block = || Block::default().style(Style::default().bg(Color::Magenta).fg(Color::Black));
f.render_widget(block(), area); f.render_widget(block(), area);
let mut column_headings = format!( let aaa = |x: &Header| {
let mut output = "";
if let Some((a, b)) = sorted_by.as_ref() {
if x == a {
output = match b {
SortedOrder::Asc => "A",
SortedOrder::Desc => "B",
};
};
};
output
};
// need to split this into blocks, and put each block in the split block, and set color to white if is selected
// then just put in the split in a horizontal fashion, with a width equal to widtrh, or char count?
// let white = "\x1b[37m";
// let reset = "\x1b[0m";
// let mut column_headings = format!(
// " {}{:>width$}{}",
// loading_icon,
// columns.state.0,
// width = columns.state.1,
// );
// Each
let mut column_headings = format!(
" {}{:>width$}", " {}{:>width$}",
loading_icon, loading_icon,
columns.state.0, columns.state.0,
@@ -381,6 +405,9 @@ pub fn draw_heading_bar<B: Backend>(
) )
.as_str(), .as_str(),
); );
// Get selected and sorted
// Maybe each heading needs to be its own boock
column_headings column_headings
.push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str()); .push_str(format!("{}{:>width$}", MARGIN, columns.cpu.0, width = columns.cpu.1).as_str());
column_headings column_headings
@@ -425,16 +452,13 @@ pub fn draw_heading_bar<B: Backend>(
); );
let suffix = if info_visible { "exit" } else { "show" }; let suffix = if info_visible { "exit" } else { "show" };
let info_text = format!("( h ) to {} help {}", suffix, MARGIN); let info_text = format!("( h ) {} help {}", suffix, MARGIN);
let info_width = info_text.chars().count(); let info_width = info_text.chars().count() as u16;
let column_width = column_headings.chars().count();
let x = area.width - info_width;
let column_width = if x > 0 { x } else { 1 };
let splits = if has_containers { let splits = if has_containers {
vec![ vec![Constraint::Min(column_width), Constraint::Min(info_width)]
Constraint::Min(column_width as u16),
Constraint::Min(info_width as u16),
]
} else { } else {
vec![Constraint::Percentage(100)] vec![Constraint::Percentage(100)]
}; };
@@ -482,6 +506,7 @@ pub fn draw_help_box<B: Backend>(f: &mut Frame<'_, B>) {
.push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line"); .push_str("\n ( ↑ ↓ ) or ( j k ) or (PgUp PgDown) or (Home End) to change selected line");
help_text.push_str("\n ( enter ) to send docker container commands"); help_text.push_str("\n ( enter ) to send docker container commands");
help_text.push_str("\n ( h ) to toggle this help information"); help_text.push_str("\n ( h ) to toggle this help information");
help_text.push_str("\n ( 1 - 9 ) order headers");
help_text.push_str( help_text.push_str(
"\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied", "\n ( m ) to toggle mouse capture - if disabled, text on screen can be selected & copied",
); );
+2
View File
@@ -157,6 +157,7 @@ fn ui<B: Backend>(
let show_help = gui_state.lock().show_help; let show_help = gui_state.lock().show_help;
let info_text = gui_state.lock().info_box_text.clone(); let info_text = gui_state.lock().info_box_text.clone();
let loading_icon = gui_state.lock().get_loading(); let loading_icon = gui_state.lock().get_loading();
let sorted_by = app_data.lock().get_sorted();
let whole_layout = Layout::default() let whole_layout = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@@ -214,6 +215,7 @@ fn ui<B: Backend>(
has_containers, has_containers,
loading_icon, loading_icon,
show_help, show_help,
sorted_by,
); );
// only draw charts if there are containers // only draw charts if there are containers