Merge branch 'feat/filter' into dev
This commit is contained in:
@@ -101,6 +101,7 @@ In application controls
|
|||||||
| ```( enter )```| Run selected docker command.|
|
| ```( enter )```| Run selected docker command.|
|
||||||
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
|
| ```( 1-9 )``` | Sort containers by heading, clicking on headings also sorts the selected column. |
|
||||||
| ```( 0 )``` | Stop sorting.|
|
| ```( 0 )``` | Stop sorting.|
|
||||||
|
| ```( F1 )``` or ```( / )``` | Toggle filter mode. |
|
||||||
| ```( e )``` | Exec into the selected container - not available on Windows.|
|
| ```( e )``` | Exec into the selected container - not available on Windows.|
|
||||||
| ```( h )``` | Toggle help menu.|
|
| ```( h )``` | Toggle help menu.|
|
||||||
| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
|
| ```( m )``` | Toggle mouse capture - if disabled, text on screen can be selected.|
|
||||||
|
|||||||
+187
-18
@@ -3,6 +3,7 @@ use core::fmt;
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use ratatui::widgets::{ListItem, ListState};
|
use ratatui::widgets::{ListItem, ListState};
|
||||||
use std::{
|
use std::{
|
||||||
|
hash::Hash,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
@@ -60,17 +61,21 @@ impl fmt::Display for Header {
|
|||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
containers: StatefulList<ContainerItem>,
|
containers: StatefulList<ContainerItem>,
|
||||||
error: Option<AppError>,
|
error: Option<AppError>,
|
||||||
|
filter_term: Option<String>,
|
||||||
sorted_by: Option<(Header, SortedOrder)>,
|
sorted_by: Option<(Header, SortedOrder)>,
|
||||||
|
hidden_containers: Vec<ContainerItem>,
|
||||||
pub args: CliArgs,
|
pub args: CliArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub struct AppData {
|
pub struct AppData {
|
||||||
|
pub hidden_containers: Vec<ContainerItem>,
|
||||||
|
pub args: CliArgs,
|
||||||
pub containers: StatefulList<ContainerItem>,
|
pub containers: StatefulList<ContainerItem>,
|
||||||
pub error: Option<AppError>,
|
pub error: Option<AppError>,
|
||||||
|
pub filter_term: Option<String>,
|
||||||
pub sorted_by: Option<(Header, SortedOrder)>,
|
pub sorted_by: Option<(Header, SortedOrder)>,
|
||||||
pub args: CliArgs,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppData {
|
impl AppData {
|
||||||
@@ -79,8 +84,10 @@ impl AppData {
|
|||||||
Self {
|
Self {
|
||||||
args,
|
args,
|
||||||
containers: StatefulList::new(vec![]),
|
containers: StatefulList::new(vec![]),
|
||||||
|
hidden_containers: vec![],
|
||||||
error: None,
|
error: None,
|
||||||
sorted_by: None,
|
sorted_by: None,
|
||||||
|
filter_term: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +100,90 @@ impl AppData {
|
|||||||
.as_secs()
|
.as_secs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Filter related methods
|
||||||
|
|
||||||
|
/// Get the current filter term
|
||||||
|
pub const fn get_filter_term(&self) -> Option<&String> {
|
||||||
|
self.filter_term.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the container name against the current filter
|
||||||
|
fn can_insert(&self, name: &str) -> bool {
|
||||||
|
self.filter_term.as_ref().map_or(true, |term| {
|
||||||
|
name.to_string()
|
||||||
|
.to_lowercase()
|
||||||
|
.contains(&term.to_lowercase())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove items from the containers list based on the filter term, and insert into a "hidden" vec
|
||||||
|
/// sets the state to start if any filtering has occured
|
||||||
|
/// Also search in the "hidden" vec for items and insert back into the main containers vec
|
||||||
|
fn filter_containers(&mut self) {
|
||||||
|
let pre_len = self.get_container_len();
|
||||||
|
|
||||||
|
if !self.hidden_containers.is_empty() {
|
||||||
|
let (mut new_items, tmp_items): (Vec<_>, Vec<_>) = self
|
||||||
|
.hidden_containers
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.partition(|item| self.can_insert(item.name.get()));
|
||||||
|
|
||||||
|
while let Some(x) = new_items.pop() {
|
||||||
|
self.containers.items.push(x);
|
||||||
|
}
|
||||||
|
self.hidden_containers = tmp_items;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (new_items, tmp_items) = self
|
||||||
|
.containers
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.partition(|item| self.can_insert(item.name.get()));
|
||||||
|
|
||||||
|
self.containers.items = new_items;
|
||||||
|
self.hidden_containers.extend(tmp_items);
|
||||||
|
|
||||||
|
self.sort_containers();
|
||||||
|
if self.get_container_len() != pre_len {
|
||||||
|
self.containers.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a single char into the filter term
|
||||||
|
pub fn filter_term_push(&mut self, c: char) {
|
||||||
|
if let Some(term) = self.filter_term.as_mut() {
|
||||||
|
term.push(c);
|
||||||
|
} else {
|
||||||
|
self.filter_term = Some(format!("{c}"));
|
||||||
|
};
|
||||||
|
self.filter_containers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete the final char of the filter term
|
||||||
|
pub fn filter_term_pop(&mut self) {
|
||||||
|
if let Some(term) = self.filter_term.as_mut() {
|
||||||
|
// should now search for items in the tmp vec, and insert into containers if found
|
||||||
|
term.pop();
|
||||||
|
if term.is_empty() {
|
||||||
|
self.filter_term = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.filter_containers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the filter term completely, empty the "hidden" container vec
|
||||||
|
pub fn filter_term_clear(&mut self) {
|
||||||
|
self.filter_term = None;
|
||||||
|
while let Some(i) = self.hidden_containers.pop() {
|
||||||
|
if self.get_container_by_id(&i.id).is_none() {
|
||||||
|
self.containers.items.push(i);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.sort_containers();
|
||||||
|
}
|
||||||
|
|
||||||
/// Container sort related methods
|
/// Container sort related methods
|
||||||
|
|
||||||
/// Change the sorted order, also set the selected container state to match new order
|
/// Change the sorted order, also set the selected container state to match new order
|
||||||
@@ -206,7 +297,7 @@ impl AppData {
|
|||||||
|
|
||||||
/// Container state methods
|
/// Container state methods
|
||||||
|
|
||||||
/// Just get the total number of containers
|
/// Get the total number of none "hidden" containers
|
||||||
pub fn get_container_len(&self) -> usize {
|
pub fn get_container_len(&self) -> usize {
|
||||||
self.containers.items.len()
|
self.containers.items.len()
|
||||||
}
|
}
|
||||||
@@ -260,8 +351,8 @@ impl AppData {
|
|||||||
let mut longest_private = 10;
|
let mut longest_private = 10;
|
||||||
let mut longest_public = 9;
|
let mut longest_public = 9;
|
||||||
|
|
||||||
for item in &self.containers.items {
|
for item in [&self.containers.items, &self.hidden_containers] {
|
||||||
// if let Some(ports) = item.ports.as_ref() {
|
for item in item {
|
||||||
longest_ip = longest_ip.max(
|
longest_ip = longest_ip.max(
|
||||||
item.ports
|
item.ports
|
||||||
.iter()
|
.iter()
|
||||||
@@ -284,11 +375,11 @@ impl AppData {
|
|||||||
.unwrap_or(6),
|
.unwrap_or(6),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
(longest_ip, longest_private, longest_public)
|
(longest_ip, longest_private, longest_public)
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Option of the current selected container's ports, sorted by private port
|
/// Get Option of the current selected container's ports, sorted by private port
|
||||||
pub fn get_selected_ports(&mut self) -> Option<(Vec<ContainerPorts>, State)> {
|
pub fn get_selected_ports(&mut self) -> Option<(Vec<ContainerPorts>, State)> {
|
||||||
if let Some(item) = self.get_mut_selected_container() {
|
if let Some(item) = self.get_mut_selected_container() {
|
||||||
@@ -307,11 +398,16 @@ impl AppData {
|
|||||||
.and_then(|i| self.containers.items.get_mut(i))
|
.and_then(|i| self.containers.items.get_mut(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// return a mutable container by given id
|
/// Get a mutable container by given id
|
||||||
fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
||||||
self.containers.items.iter_mut().find(|i| &i.id == id)
|
self.containers.items.iter_mut().find(|i| &i.id == id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a mutable container by given id in the tmp_container vec
|
||||||
|
fn get_hidden_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
||||||
|
self.hidden_containers.iter_mut().find(|i| &i.id == id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the ContainerName of by ID
|
/// Get the ContainerName of by ID
|
||||||
pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<ContainerName> {
|
pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option<ContainerName> {
|
||||||
self.containers
|
self.containers
|
||||||
@@ -333,6 +429,7 @@ impl AppData {
|
|||||||
self.get_selected_container()
|
self.get_selected_container()
|
||||||
.map(|i| (i.id.clone(), i.state, i.name.get().to_owned()))
|
.map(|i| (i.id.clone(), i.state, i.name.get().to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selected DockerCommand methods
|
/// Selected DockerCommand methods
|
||||||
|
|
||||||
/// Get the current selected docker command
|
/// Get the current selected docker command
|
||||||
@@ -467,17 +564,17 @@ impl AppData {
|
|||||||
|
|
||||||
/// Error related methods
|
/// Error related methods
|
||||||
|
|
||||||
/// return single app_state error
|
/// Get single app_state error
|
||||||
pub const fn get_error(&self) -> Option<AppError> {
|
pub const fn get_error(&self) -> Option<AppError> {
|
||||||
self.error
|
self.error
|
||||||
}
|
}
|
||||||
|
|
||||||
/// remove single app_state error
|
/// Remove single app_state error
|
||||||
pub fn remove_error(&mut self) {
|
pub fn remove_error(&mut self) {
|
||||||
self.error = None;
|
self.error = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// insert single app_state error
|
/// Insert single app_state error
|
||||||
pub fn set_error(&mut self, error: AppError, gui_state: &Arc<Mutex<GuiState>>, status: Status) {
|
pub fn set_error(&mut self, error: AppError, gui_state: &Arc<Mutex<GuiState>>, status: Status) {
|
||||||
gui_state.lock().status_push(status);
|
gui_state.lock().status_push(status);
|
||||||
self.error = Some(error);
|
self.error = Some(error);
|
||||||
@@ -498,12 +595,14 @@ impl AppData {
|
|||||||
|
|
||||||
/// Find the widths for the strings in the containers panel.
|
/// Find the widths for the strings in the containers panel.
|
||||||
/// So can display nicely and evenly
|
/// So can display nicely and evenly
|
||||||
|
/// Searches in both containes & hidden_containers
|
||||||
pub fn get_width(&self) -> Columns {
|
pub fn get_width(&self) -> Columns {
|
||||||
let mut columns = Columns::new();
|
let mut columns = Columns::new();
|
||||||
let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12);
|
let count = |x: &str| u8::try_from(x.chars().count()).unwrap_or(12);
|
||||||
|
|
||||||
// Should probably find a refactor here somewhere
|
// Should probably find a refactor here somewhere
|
||||||
for container in &self.containers.items {
|
for container in [&self.containers.items, &self.hidden_containers] {
|
||||||
|
for container in container {
|
||||||
let cpu_count = count(
|
let cpu_count = count(
|
||||||
&container
|
&container
|
||||||
.cpu_stats
|
.cpu_stats
|
||||||
@@ -520,7 +619,6 @@ impl AppData {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Issue here!
|
|
||||||
columns.cpu.1 = columns.cpu.1.max(cpu_count);
|
columns.cpu.1 = columns.cpu.1.max(cpu_count);
|
||||||
columns.image.1 = columns.image.1.max(count(&container.image.to_string()));
|
columns.image.1 = columns.image.1.max(count(&container.image.to_string()));
|
||||||
columns.mem.1 = columns.mem.1.max(mem_current_count);
|
columns.mem.1 = columns.mem.1.max(mem_current_count);
|
||||||
@@ -531,11 +629,21 @@ impl AppData {
|
|||||||
columns.state.1 = columns.state.1.max(count(&container.state.to_string()));
|
columns.state.1 = columns.state.1.max(count(&container.state.to_string()));
|
||||||
columns.status.1 = columns.status.1.max(count(&container.status));
|
columns.status.1 = columns.status.1.max(count(&container.status));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
columns
|
columns
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update related methods
|
/// Update related methods
|
||||||
|
|
||||||
|
/// Get mutable reference to a container in the containers vec & the hidden_containers vec
|
||||||
|
fn get_any_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> {
|
||||||
|
if self.get_hidden_container_by_id(id).is_some() {
|
||||||
|
self.get_hidden_container_by_id(id)
|
||||||
|
} else {
|
||||||
|
self.get_container_by_id(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
|
/// Update container mem, cpu, & network stats, in single function so only need to call .lock() once
|
||||||
/// Will also, if a sort is set, sort the containers
|
/// Will also, if a sort is set, sort the containers
|
||||||
pub fn update_stats_by_id(
|
pub fn update_stats_by_id(
|
||||||
@@ -547,7 +655,7 @@ impl AppData {
|
|||||||
rx: u64,
|
rx: u64,
|
||||||
tx: u64,
|
tx: u64,
|
||||||
) {
|
) {
|
||||||
if let Some(container) = self.get_container_by_id(id) {
|
if let Some(container) = self.get_any_container_by_id(id) {
|
||||||
if container.cpu_stats.len() >= 60 {
|
if container.cpu_stats.len() >= 60 {
|
||||||
container.cpu_stats.pop_front();
|
container.cpu_stats.pop_front();
|
||||||
}
|
}
|
||||||
@@ -642,8 +750,8 @@ impl AppData {
|
|||||||
let created = i
|
let created = i
|
||||||
.created
|
.created
|
||||||
.map_or(0, |i| u64::try_from(i).unwrap_or_default());
|
.map_or(0, |i| u64::try_from(i).unwrap_or_default());
|
||||||
// If container info already in containers Vec, then just update details
|
|
||||||
if let Some(item) = self.get_container_by_id(&id) {
|
if let Some(item) = self.get_any_container_by_id(&id) {
|
||||||
if item.name.get() != name {
|
if item.name.get() != name {
|
||||||
item.name.set(name);
|
item.name.set(name);
|
||||||
};
|
};
|
||||||
@@ -668,24 +776,29 @@ impl AppData {
|
|||||||
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 Ve
|
||||||
|
let can_insert = self.can_insert(&name);
|
||||||
let container = ContainerItem::new(
|
let container = ContainerItem::new(
|
||||||
created, id, image, is_oxker, name, ports, state, status,
|
created, id, image, is_oxker, name, ports, state, status,
|
||||||
);
|
);
|
||||||
|
if can_insert {
|
||||||
self.containers.items.push(container);
|
self.containers.items.push(container);
|
||||||
|
} else {
|
||||||
|
self.hidden_containers.push(container);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// update logs of a given container, based on id
|
/// Update logs of a given container, based on id
|
||||||
pub fn update_log_by_id(&mut self, logs: Vec<String>, id: &ContainerId) {
|
pub fn update_log_by_id(&mut self, logs: Vec<String>, id: &ContainerId) {
|
||||||
let color = self.args.color;
|
let color = self.args.color;
|
||||||
let raw = self.args.raw;
|
let raw = self.args.raw;
|
||||||
|
|
||||||
let timestamp = self.args.timestamp;
|
let timestamp = self.args.timestamp;
|
||||||
|
|
||||||
if let Some(container) = self.get_container_by_id(id) {
|
if let Some(container) = self.get_any_container_by_id(id) {
|
||||||
if !container.is_oxker {
|
if !container.is_oxker {
|
||||||
container.last_updated = Self::get_systemtime();
|
container.last_updated = Self::get_systemtime();
|
||||||
let current_len = container.logs.len();
|
let current_len = container.logs.len();
|
||||||
@@ -1433,6 +1546,36 @@ mod tests {
|
|||||||
test_state(State::Unknown, &mut vec![DockerControls::Delete]);
|
test_state(State::Unknown, &mut vec![DockerControls::Delete]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ****** //
|
||||||
|
// Filter //
|
||||||
|
// ****** //
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Data is filtered correctly
|
||||||
|
fn test_app_data_filter() {
|
||||||
|
let (_, containers) = gen_containers();
|
||||||
|
|
||||||
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
|
assert!(app_data.get_filter_term().is_none());
|
||||||
|
|
||||||
|
let pre_len = app_data.containers.items.len();
|
||||||
|
app_data.filter_term_push('_');
|
||||||
|
app_data.filter_term_push('2');
|
||||||
|
|
||||||
|
assert_eq!(app_data.get_filter_term(), Some(&"_2".to_string()));
|
||||||
|
|
||||||
|
app_data.filter_containers();
|
||||||
|
let post_len = app_data.containers.items.len();
|
||||||
|
assert!(pre_len != post_len);
|
||||||
|
assert_eq!(post_len, 1);
|
||||||
|
|
||||||
|
// Can insert checks against the current filter term
|
||||||
|
assert!(app_data.can_insert("_2"));
|
||||||
|
assert!(!app_data.can_insert("_"));
|
||||||
|
assert!(!app_data.can_insert("_3"));
|
||||||
|
}
|
||||||
|
|
||||||
// **** //
|
// **** //
|
||||||
// Logs //
|
// Logs //
|
||||||
// **** //
|
// **** //
|
||||||
@@ -1732,6 +1875,32 @@ mod tests {
|
|||||||
assert_eq!(result, expected);
|
assert_eq!(result, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Header widths return correctly when some containers hidden
|
||||||
|
fn test_app_data_get_width_filtered() {
|
||||||
|
let (_ids, mut containers) = gen_containers();
|
||||||
|
containers[0].name = ContainerName::from("some_longer_name_with_filter");
|
||||||
|
let mut app_data = gen_appdata(&containers);
|
||||||
|
|
||||||
|
let result = app_data.get_width();
|
||||||
|
let expected = Columns {
|
||||||
|
name: (Header::Name, 28),
|
||||||
|
state: (Header::State, 11),
|
||||||
|
status: (Header::Status, 16),
|
||||||
|
cpu: (Header::Cpu, 7),
|
||||||
|
mem: (Header::Memory, 7, 7),
|
||||||
|
id: (Header::Id, 8),
|
||||||
|
image: (Header::Image, 7),
|
||||||
|
net_rx: (Header::Rx, 7),
|
||||||
|
net_tx: (Header::Tx, 7),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
app_data.filter_term_push('c');
|
||||||
|
app_data.filter_containers();
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
// ***** //
|
// ***** //
|
||||||
// Ports //
|
// Ports //
|
||||||
// ***** //
|
// ***** //
|
||||||
|
|||||||
+15
-16
@@ -303,13 +303,14 @@ impl DockerData {
|
|||||||
};
|
};
|
||||||
self.update_all_container_stats(&all_ids);
|
self.update_all_container_stats(&all_ids);
|
||||||
self.app_data.lock().sort_containers();
|
self.app_data.lock().sort_containers();
|
||||||
|
self.gui_state.lock().stop_loading_animation(Uuid::nil());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize docker container data, before any messages are received
|
/// Initialize docker container data, before any messages are received
|
||||||
async fn initialise_container_data(&mut self) {
|
async fn initialise_container_data(&mut self) {
|
||||||
self.gui_state.lock().status_push(Status::Init);
|
self.gui_state.lock().status_push(Status::Init);
|
||||||
let loading_uuid = Uuid::new_v4();
|
let loading_uuid = Uuid::new_v4();
|
||||||
let loading_handle = GuiState::start_loading_animation(&self.gui_state, loading_uuid);
|
GuiState::start_loading_animation(&self.gui_state, loading_uuid);
|
||||||
let all_ids = self.update_all_containers().await;
|
let all_ids = self.update_all_containers().await;
|
||||||
|
|
||||||
self.update_all_container_stats(&all_ids);
|
self.update_all_container_stats(&all_ids);
|
||||||
@@ -323,9 +324,7 @@ impl DockerData {
|
|||||||
self.init = None;
|
self.init = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.gui_state
|
self.gui_state.lock().stop_loading_animation(loading_uuid);
|
||||||
.lock()
|
|
||||||
.stop_loading_animation(&loading_handle, loading_uuid);
|
|
||||||
self.gui_state.lock().status_del(Status::Init);
|
self.gui_state.lock().status_del(Status::Init);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,27 +355,27 @@ impl DockerData {
|
|||||||
}
|
}
|
||||||
DockerMessage::Pause(id) => {
|
DockerMessage::Pause(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker.pause_container(id.get()).await.is_err() {
|
if docker.pause_container(id.get()).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Pause, &gui_state);
|
Self::set_error(&app_data, DockerControls::Pause, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Restart(id) => {
|
DockerMessage::Restart(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker.restart_container(id.get(), None).await.is_err() {
|
if docker.restart_container(id.get(), None).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Restart, &gui_state);
|
Self::set_error(&app_data, DockerControls::Restart, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Start(id) => {
|
DockerMessage::Start(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker
|
if docker
|
||||||
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
.start_container(id.get(), None::<StartContainerOptions<String>>)
|
||||||
.await
|
.await
|
||||||
@@ -384,33 +383,33 @@ impl DockerData {
|
|||||||
{
|
{
|
||||||
Self::set_error(&app_data, DockerControls::Start, &gui_state);
|
Self::set_error(&app_data, DockerControls::Start, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Stop(id) => {
|
DockerMessage::Stop(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker.stop_container(id.get(), None).await.is_err() {
|
if docker.stop_container(id.get(), None).await.is_err() {
|
||||||
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Resume(id) => {
|
DockerMessage::Resume(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
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::Resume, &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(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
}
|
}
|
||||||
DockerMessage::Delete(id) => {
|
DockerMessage::Delete(id) => {
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let handle = GuiState::start_loading_animation(&gui_state, uuid);
|
GuiState::start_loading_animation(&gui_state, uuid);
|
||||||
if docker
|
if docker
|
||||||
.remove_container(
|
.remove_container(
|
||||||
id.get(),
|
id.get(),
|
||||||
@@ -425,7 +424,7 @@ impl DockerData {
|
|||||||
{
|
{
|
||||||
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
Self::set_error(&app_data, DockerControls::Stop, &gui_state);
|
||||||
}
|
}
|
||||||
gui_state.lock().stop_loading_animation(&handle, uuid);
|
gui_state.lock().stop_loading_animation(uuid);
|
||||||
});
|
});
|
||||||
self.update_everything().await;
|
self.update_everything().await;
|
||||||
self.gui_state.lock().set_delete_container(None);
|
self.gui_state.lock().set_delete_container(None);
|
||||||
|
|||||||
+83
-36
@@ -71,6 +71,7 @@ impl InputHandler {
|
|||||||
Status::Error,
|
Status::Error,
|
||||||
Status::Help,
|
Status::Help,
|
||||||
Status::DeleteConfirm,
|
Status::DeleteConfirm,
|
||||||
|
Status::Filter,
|
||||||
]) {
|
]) {
|
||||||
self.mouse_press(mouse_event);
|
self.mouse_press(mouse_event);
|
||||||
}
|
}
|
||||||
@@ -125,7 +126,7 @@ impl InputHandler {
|
|||||||
let is_oxker = self.app_data.lock().is_oxker();
|
let is_oxker = self.app_data.lock().is_oxker();
|
||||||
if !is_oxker && tty_readable() {
|
if !is_oxker && tty_readable() {
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
let handle = GuiState::start_loading_animation(&self.gui_state, uuid);
|
GuiState::start_loading_animation(&self.gui_state, uuid);
|
||||||
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
|
let (sx, rx) = tokio::sync::oneshot::channel::<Arc<Docker>>();
|
||||||
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
|
self.docker_tx.send(DockerMessage::Exec(sx)).await.ok();
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ impl InputHandler {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.gui_state.lock().stop_loading_animation(&handle, uuid);
|
self.gui_state.lock().stop_loading_animation(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +249,7 @@ impl InputHandler {
|
|||||||
self.gui_state.lock().status_push(log_status);
|
self.gui_state.lock().status_push(log_status);
|
||||||
|
|
||||||
let uuid = Uuid::new_v4();
|
let uuid = Uuid::new_v4();
|
||||||
let handle = GuiState::start_loading_animation(&self.gui_state, uuid);
|
GuiState::start_loading_animation(&self.gui_state, uuid);
|
||||||
if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
|
if save_logs(&self.app_data, &self.gui_state, &self.docker_tx)
|
||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
@@ -260,7 +261,7 @@ impl InputHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.gui_state.lock().status_del(log_status);
|
self.gui_state.lock().status_del(log_status);
|
||||||
self.gui_state.lock().stop_loading_animation(&handle, uuid);
|
self.gui_state.lock().stop_loading_animation(uuid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,36 +354,8 @@ impl InputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle keyboard button events
|
/// Actions to take when in Help status active
|
||||||
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
|
fn handle_help(&mut self, key_code: KeyCode) {
|
||||||
let contains_delete = self
|
|
||||||
.gui_state
|
|
||||||
.lock()
|
|
||||||
.status_contains(&[Status::DeleteConfirm]);
|
|
||||||
|
|
||||||
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
|
|
||||||
|
|
||||||
let contains_error = contains(Status::Error);
|
|
||||||
let contains_help = contains(Status::Help);
|
|
||||||
let contains_exec = contains(Status::Exec);
|
|
||||||
|
|
||||||
if !contains_exec {
|
|
||||||
// Always just quit on Ctrl + c/C or q/Q
|
|
||||||
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
|
||||||
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
|
||||||
if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() {
|
|
||||||
self.quit().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if contains_error {
|
|
||||||
match key_code {
|
|
||||||
KeyCode::Esc | KeyCode::Char('c' | 'C') => {
|
|
||||||
self.app_data.lock().remove_error();
|
|
||||||
self.gui_state.lock().status_del(Status::Error);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
} else if contains_help {
|
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Esc | KeyCode::Char('h' | 'H') => {
|
KeyCode::Esc | KeyCode::Char('h' | 'H') => {
|
||||||
self.gui_state.lock().status_del(Status::Help);
|
self.gui_state.lock().status_del(Status::Help);
|
||||||
@@ -390,13 +363,50 @@ impl InputHandler {
|
|||||||
KeyCode::Char('m' | 'M') => self.m_key(),
|
KeyCode::Char('m' | 'M') => self.m_key(),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
} else if contains_delete {
|
}
|
||||||
|
|
||||||
|
/// Actions to take when Error status active
|
||||||
|
fn handle_error(&mut self, key_code: KeyCode) {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::Esc | KeyCode::Char('c' | 'C') => {
|
||||||
|
self.app_data.lock().remove_error();
|
||||||
|
self.gui_state.lock().status_del(Status::Error);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actions to take when Delete status active
|
||||||
|
async fn handle_delete(&mut self, key_code: KeyCode) {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
KeyCode::Char('y' | 'Y') => self.confirm_delete().await,
|
||||||
KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(),
|
KeyCode::Esc | KeyCode::Char('n' | 'N') => self.clear_delete(),
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
/// Actions to take when Filter status active
|
||||||
|
fn handle_filter(&mut self, key_code: KeyCode) {
|
||||||
|
match key_code {
|
||||||
|
KeyCode::F(1) | KeyCode::Char('/') | KeyCode::Esc => {
|
||||||
|
self.app_data.lock().filter_term_clear();
|
||||||
|
self.gui_state.lock().status_del(Status::Filter);
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.gui_state.lock().status_del(Status::Filter);
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
self.app_data.lock().filter_term_pop();
|
||||||
|
}
|
||||||
|
KeyCode::Char(x) => {
|
||||||
|
self.app_data.lock().filter_term_push(x);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle button presses in all other scenarios
|
||||||
|
async fn handle_others(&mut self, key_code: KeyCode) {
|
||||||
match key_code {
|
match key_code {
|
||||||
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
KeyCode::Char('0') => self.app_data.lock().reset_sorted(),
|
||||||
KeyCode::Char('1') => self.sort(Header::Name),
|
KeyCode::Char('1') => self.sort(Header::Name),
|
||||||
@@ -422,6 +432,10 @@ impl InputHandler {
|
|||||||
self.previous();
|
self.previous();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyCode::F(1) | KeyCode::Char('/') => {
|
||||||
|
self.gui_state.lock().status_push(Status::Filter);
|
||||||
|
self.docker_tx.send(DockerMessage::Update).await.ok();
|
||||||
|
}
|
||||||
KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
|
KeyCode::Down | KeyCode::Char('j' | 'J') => self.next(),
|
||||||
KeyCode::PageDown => {
|
KeyCode::PageDown => {
|
||||||
for _ in 0..=6 {
|
for _ in 0..=6 {
|
||||||
@@ -432,6 +446,39 @@ impl InputHandler {
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Handle keyboard button events
|
||||||
|
async fn button_press(&mut self, key_code: KeyCode, key_modifier: KeyModifiers) {
|
||||||
|
let contains_delete = self
|
||||||
|
.gui_state
|
||||||
|
.lock()
|
||||||
|
.status_contains(&[Status::DeleteConfirm]);
|
||||||
|
|
||||||
|
let contains = |s: Status| self.gui_state.lock().status_contains(&[s]);
|
||||||
|
|
||||||
|
let contains_error = contains(Status::Error);
|
||||||
|
let contains_help = contains(Status::Help);
|
||||||
|
let contains_exec = contains(Status::Exec);
|
||||||
|
let contains_filter: bool = contains(Status::Filter);
|
||||||
|
|
||||||
|
if !contains_exec {
|
||||||
|
let is_c = || key_code == KeyCode::Char('c') || key_code == KeyCode::Char('C');
|
||||||
|
let is_q = || key_code == KeyCode::Char('q') || key_code == KeyCode::Char('Q');
|
||||||
|
if key_modifier == KeyModifiers::CONTROL && is_c() || is_q() && !contains_filter {
|
||||||
|
// Always just quit on Ctrl + c/C or q/Q, unless in FIlter status active
|
||||||
|
self.quit().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if contains_error {
|
||||||
|
self.handle_error(key_code);
|
||||||
|
} else if contains_help {
|
||||||
|
self.handle_help(key_code);
|
||||||
|
} else if contains_filter {
|
||||||
|
self.handle_filter(key_code);
|
||||||
|
} else if contains_delete {
|
||||||
|
self.handle_delete(key_code).await;
|
||||||
|
} else {
|
||||||
|
self.handle_others(key_code).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,11 @@ 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 std::{
|
||||||
|
collections::{HashSet, VecDeque},
|
||||||
|
vec,
|
||||||
|
};
|
||||||
|
|
||||||
use bollard::service::{ContainerSummary, Port};
|
use bollard::service::{ContainerSummary, Port};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -209,8 +214,10 @@ mod tests {
|
|||||||
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
|
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
|
||||||
AppData {
|
AppData {
|
||||||
containers: StatefulList::new(containers.to_vec()),
|
containers: StatefulList::new(containers.to_vec()),
|
||||||
|
hidden_containers: vec![],
|
||||||
error: None,
|
error: None,
|
||||||
sorted_by: None,
|
sorted_by: None,
|
||||||
|
filter_term: None,
|
||||||
args: gen_args(),
|
args: gen_args(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+185
-9
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
gui_state::{BoxLocation, DeleteButton, Region},
|
gui_state::{BoxLocation, DeleteButton, Region},
|
||||||
FrameData,
|
FrameData, Status,
|
||||||
};
|
};
|
||||||
use super::{GuiState, SelectablePanel};
|
use super::{GuiState, SelectablePanel};
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ fn generate_block<'a>(
|
|||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.title(title);
|
.title(title);
|
||||||
if fd.selected_panel == panel {
|
if fd.selected_panel == panel && !gui_state.lock().status_contains(&[Status::Filter]) {
|
||||||
block = block.border_style(Style::default().fg(Color::LightCyan));
|
block = block.border_style(Style::default().fg(Color::LightCyan));
|
||||||
}
|
}
|
||||||
block
|
block
|
||||||
@@ -233,7 +233,15 @@ pub fn containers(
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
let paragraph = Paragraph::new("no containers running")
|
let text = if app_data.lock().get_filter_term().is_some() {
|
||||||
|
"no containers match filter"
|
||||||
|
} else if gui_state.lock().is_loading() {
|
||||||
|
&format!("loading {}", fd.loading_icon)
|
||||||
|
} else {
|
||||||
|
"no containers running"
|
||||||
|
};
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
.block(block)
|
.block(block)
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
f.render_widget(paragraph, area);
|
f.render_widget(paragraph, area);
|
||||||
@@ -414,6 +422,32 @@ fn make_chart<'a, T: Stats + Display>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draw the filter bar
|
||||||
|
pub fn filter_bar(area: Rect, frame: &mut Frame, app_data: &Arc<Mutex<AppData>>) {
|
||||||
|
let style_but = Style::default().fg(Color::Black).bg(Color::Magenta);
|
||||||
|
let style_desc = Style::default().fg(Color::Gray).bg(Color::Reset);
|
||||||
|
let line = Line::from(vec![
|
||||||
|
Span::styled(" Enter ", style_but),
|
||||||
|
Span::styled(" done ", style_desc),
|
||||||
|
Span::styled(" Esc ", style_but),
|
||||||
|
Span::styled(" clear ", style_desc),
|
||||||
|
Span::styled(
|
||||||
|
"filter: ",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Magenta)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::styled(
|
||||||
|
app_data
|
||||||
|
.lock()
|
||||||
|
.get_filter_term()
|
||||||
|
.map_or(String::new(), std::borrow::ToOwned::to_owned),
|
||||||
|
Style::default().fg(Color::Gray),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
frame.render_widget(line, area);
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw heading bar at top of program, always visible
|
/// Draw heading bar at top of program, always visible
|
||||||
/// TODO Should separate into loading icon/headers/help functions
|
/// TODO Should separate into loading icon/headers/help functions
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
@@ -521,6 +555,11 @@ pub fn heading_bar(
|
|||||||
.constraints(splits)
|
.constraints(splits)
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
|
// Draw loading icon, or not, and a prefix with a single space
|
||||||
|
let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
|
||||||
|
.block(block(Color::White))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
frame.render_widget(loading_paragraph, split_bar[0]);
|
||||||
if data.has_containers {
|
if data.has_containers {
|
||||||
let header_section_width = split_bar[1].width;
|
let header_section_width = split_bar[1].width;
|
||||||
|
|
||||||
@@ -540,11 +579,11 @@ pub fn heading_bar(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Draw loading icon, or not, and a prefix with a single space
|
// // Draw loading icon, or not, and a prefix with a single space
|
||||||
let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
|
// let loading_paragraph = Paragraph::new(format!("{:>2}", data.loading_icon))
|
||||||
.block(block(Color::White))
|
// .block(block(Color::White))
|
||||||
.alignment(Alignment::Center);
|
// .alignment(Alignment::Center);
|
||||||
frame.render_widget(loading_paragraph, split_bar[0]);
|
// frame.render_widget(loading_paragraph, split_bar[0]);
|
||||||
|
|
||||||
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
||||||
let headers_section = Layout::default()
|
let headers_section = Layout::default()
|
||||||
@@ -701,6 +740,13 @@ impl HelpInfo {
|
|||||||
"toggle mouse capture - if disabled, text on screen can be selected & copied",
|
"toggle mouse capture - if disabled, text on screen can be selected & copied",
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
space(),
|
||||||
|
button_item("F1"),
|
||||||
|
or(),
|
||||||
|
button_item("/"),
|
||||||
|
button_desc("toggle filter mode"),
|
||||||
|
]),
|
||||||
Line::from(vec![space(), button_item("0"), button_desc("stop sort")]),
|
Line::from(vec![space(), button_item("0"), button_desc("stop sort")]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
space(),
|
space(),
|
||||||
@@ -2461,7 +2507,7 @@ mod tests {
|
|||||||
/// This will cause issues once the version has more than the current 5 chars (0.5.0)
|
/// This will cause issues once the version has more than the current 5 chars (0.5.0)
|
||||||
// Help popup is drawn correctly
|
// Help popup is drawn correctly
|
||||||
fn test_draw_blocks_help() {
|
fn test_draw_blocks_help() {
|
||||||
let (w, h) = (87, 32);
|
let (w, h) = (87, 33);
|
||||||
let mut setup = test_setup(w, h, true, true);
|
let mut setup = test_setup(w, h, true, true);
|
||||||
|
|
||||||
setup
|
setup
|
||||||
@@ -2492,6 +2538,7 @@ mod tests {
|
|||||||
" │ ( h ) toggle this help information │ ".to_owned(),
|
" │ ( h ) toggle this help information │ ".to_owned(),
|
||||||
" │ ( s ) save logs to file │ ".to_owned(),
|
" │ ( s ) save logs to file │ ".to_owned(),
|
||||||
" │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ".to_owned(),
|
" │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ".to_owned(),
|
||||||
|
" │ ( F1 ) or ( / ) toggle filter mode │ ".to_owned(),
|
||||||
" │ ( 0 ) stop sort │ ".to_owned(),
|
" │ ( 0 ) stop sort │ ".to_owned(),
|
||||||
" │ ( 1 - 9 ) sort by header - or click header │ ".to_owned(),
|
" │ ( 1 - 9 ) sort by header - or click header │ ".to_owned(),
|
||||||
" │ ( esc ) close dialog │ ".to_owned(),
|
" │ ( esc ) close dialog │ ".to_owned(),
|
||||||
@@ -2502,6 +2549,7 @@ mod tests {
|
|||||||
" │ │ ".to_owned(),
|
" │ │ ".to_owned(),
|
||||||
" │ │ ".to_owned(),
|
" │ │ ".to_owned(),
|
||||||
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ".to_owned(),
|
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ".to_owned(),
|
||||||
|
" ".to_owned(),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (row_index, row) in expected.iter().enumerate() {
|
for (row_index, row) in expected.iter().enumerate() {
|
||||||
@@ -3148,6 +3196,134 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
/// Check that the whole layout is drawn correctly
|
||||||
|
fn test_draw_blocks_whole_layout_with_filter() {
|
||||||
|
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[1]
|
||||||
|
.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 ───────────────────────╮╭────────── ports ───────────╮",
|
||||||
|
"│10.00%│ •••• ││100.00 kB│ ••• ││ ip private public│",
|
||||||
|
"│ │ ••• • ││ │ ••• • ││ 8001 │",
|
||||||
|
"│ │•• ••• ││ │•• ••• ││ │",
|
||||||
|
"│ │ ││ │ ││ │",
|
||||||
|
"╰───────────────────────────────────────────────────────────────╯╰───────────────────────────────────────────────────────────────╯╰────────────────────────────╯",
|
||||||
|
];
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
draw_frame(f, &setup.app_data, &setup.gui_state);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = &setup.terminal.backend().buffer().content;
|
||||||
|
|
||||||
|
for (row_index, row) in result.chunks(usize::from(w)).enumerate() {
|
||||||
|
let expected_row = expected[row_index]
|
||||||
|
.chars()
|
||||||
|
.map(|i| i.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (cell_index, cell) in row.iter().enumerate() {
|
||||||
|
assert_eq!(cell.symbol(), expected_row[cell_index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 expected = [
|
||||||
|
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||||
|
"╭ Containers 1/1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮",
|
||||||
|
"│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │",
|
||||||
|
"│ ││ restart │",
|
||||||
|
"│ ││ 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 │",
|
||||||
|
"│ │ ••• • • ││ │ ••• •• ││ │",
|
||||||
|
"│ │• •• ││ │• • ││ │",
|
||||||
|
"│ │ ││ │ ││ │",
|
||||||
|
"╰─────────────────────────────────────────────────────────────────╯╰─────────────────────────────────────────────────────────────────╯╰────────────────────────╯",
|
||||||
|
" Enter done Esc clear filter: r_1 ",
|
||||||
|
];
|
||||||
|
setup
|
||||||
|
.terminal
|
||||||
|
.draw(|f| {
|
||||||
|
draw_frame(f, &setup.app_data, &setup.gui_state);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = &setup.terminal.backend().buffer().content;
|
||||||
|
|
||||||
|
for (row_index, row) in result.chunks(usize::from(w)).enumerate() {
|
||||||
|
let expected_row = expected[row_index]
|
||||||
|
.chars()
|
||||||
|
.map(|i| i.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (cell_index, cell) in row.iter().enumerate() {
|
||||||
|
assert_eq!(cell.symbol(), expected_row[cell_index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Check that the whole layout is drawn correctly when have long container name and long image name
|
/// 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() {
|
fn test_draw_blocks_whole_layout_long_name() {
|
||||||
|
|||||||
+29
-26
@@ -163,19 +163,21 @@ pub enum Status {
|
|||||||
DockerConnect,
|
DockerConnect,
|
||||||
Error,
|
Error,
|
||||||
Exec,
|
Exec,
|
||||||
|
Filter,
|
||||||
Help,
|
Help,
|
||||||
Init,
|
Init,
|
||||||
Logs,
|
Logs,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global gui_state, stored in an Arc<Mutex>
|
/// Global gui_state, stored in an Arc<Mutex>
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default)]
|
||||||
pub struct GuiState {
|
pub struct GuiState {
|
||||||
delete_container: Option<ContainerId>,
|
delete_container: Option<ContainerId>,
|
||||||
delete_map: HashMap<DeleteButton, Rect>,
|
delete_map: HashMap<DeleteButton, Rect>,
|
||||||
heading_map: HashMap<Header, Rect>,
|
heading_map: HashMap<Header, Rect>,
|
||||||
is_loading: HashSet<Uuid>,
|
loading_handle: Option<JoinHandle<()>>,
|
||||||
loading_index: u8,
|
loading_index: u8,
|
||||||
|
loading_set: HashSet<Uuid>,
|
||||||
panel_map: HashMap<SelectablePanel, Rect>,
|
panel_map: HashMap<SelectablePanel, Rect>,
|
||||||
selected_panel: SelectablePanel,
|
selected_panel: SelectablePanel,
|
||||||
status: HashSet<Status>,
|
status: HashSet<Status>,
|
||||||
@@ -325,45 +327,46 @@ impl GuiState {
|
|||||||
} else {
|
} else {
|
||||||
self.loading_index += 1;
|
self.loading_index += 1;
|
||||||
}
|
}
|
||||||
self.is_loading.insert(uuid);
|
self.loading_set.insert(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_loading(&self) -> bool {
|
||||||
|
!self.loading_set.is_empty()
|
||||||
|
}
|
||||||
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
|
/// If is_loading has any entries, return the char at FRAMES[index], else an empty char, which needs to take up the same space, hence ' '
|
||||||
pub fn get_loading(&self) -> char {
|
pub fn get_loading(&self) -> char {
|
||||||
if self.is_loading.is_empty() {
|
if self.is_loading() {
|
||||||
' '
|
|
||||||
} else {
|
|
||||||
FRAMES[usize::from(self.loading_index)]
|
FRAMES[usize::from(self.loading_index)]
|
||||||
}
|
} else {
|
||||||
}
|
' '
|
||||||
|
|
||||||
/// Remove a loading_uuid from the is_loading HashSet, if empty, reset loading_index to 0
|
|
||||||
fn remove_loading(&mut self, uuid: Uuid) {
|
|
||||||
self.is_loading.remove(&uuid);
|
|
||||||
if self.is_loading.is_empty() {
|
|
||||||
self.loading_index = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Animate the loading icon in its own Tokio thread
|
/// Animate the loading icon in its own Tokio thread
|
||||||
pub fn start_loading_animation(
|
/// This should only be able to executed once, rather than multiple spawns
|
||||||
gui_state: &Arc<Mutex<Self>>,
|
pub fn start_loading_animation(gui_state: &Arc<Mutex<Self>>, loading_uuid: Uuid) {
|
||||||
loading_uuid: Uuid,
|
if !gui_state.lock().is_loading() {
|
||||||
) -> JoinHandle<()> {
|
let inner_state = Arc::clone(gui_state);
|
||||||
gui_state.lock().next_loading(loading_uuid);
|
gui_state.lock().loading_handle = Some(tokio::spawn(async move {
|
||||||
let gui_state = Arc::clone(gui_state);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
gui_state.lock().next_loading(loading_uuid);
|
inner_state.lock().next_loading(loading_uuid);
|
||||||
}
|
}
|
||||||
})
|
}));
|
||||||
|
}
|
||||||
|
gui_state.lock().next_loading(loading_uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop the loading_spin function, and reset gui loading status
|
/// Stop the loading_spin function, and reset gui loading status
|
||||||
pub fn stop_loading_animation(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) {
|
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
|
||||||
handle.abort();
|
self.loading_set.remove(&loading_uuid);
|
||||||
self.remove_loading(loading_uuid);
|
if self.loading_set.is_empty() {
|
||||||
|
self.loading_index = 0;
|
||||||
|
if let Some(h) = &self.loading_handle {
|
||||||
|
h.abort();
|
||||||
|
}
|
||||||
|
self.loading_handle = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set info box content
|
/// Set info box content
|
||||||
|
|||||||
+15
-5
@@ -64,7 +64,6 @@ impl Ui {
|
|||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
) {
|
) {
|
||||||
if let Ok(mut terminal) = Self::setup_terminal() {
|
if let Ok(mut terminal) = Self::setup_terminal() {
|
||||||
// let args = app_data.lock().args.clone();
|
|
||||||
let cursor_position = terminal.get_cursor().unwrap_or_default();
|
let cursor_position = terminal.get_cursor().unwrap_or_default();
|
||||||
let mut ui = Self {
|
let mut ui = Self {
|
||||||
app_data,
|
app_data,
|
||||||
@@ -264,14 +263,20 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
|
|||||||
/// Draw the main ui to a frame of the terminal
|
/// Draw the main ui to a frame of the terminal
|
||||||
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 contains_filter = gui_state.lock().status_contains(&[Status::Filter]);
|
||||||
|
|
||||||
|
let whole_constraints = if contains_filter {
|
||||||
|
vec![Constraint::Max(1), Constraint::Min(1), Constraint::Max(1)]
|
||||||
|
} else {
|
||||||
|
vec![Constraint::Max(1), Constraint::Min(1)]
|
||||||
|
};
|
||||||
|
|
||||||
let whole_layout = Layout::default()
|
let whole_layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Max(1), Constraint::Min(1)].as_ref())
|
.constraints(whole_constraints)
|
||||||
.split(f.size());
|
.split(f.size());
|
||||||
|
|
||||||
// Split into 3, containers+controls, logs, then graphs
|
// Split into 3, containers+controls, logs, then graphs
|
||||||
// This one is the issue!
|
|
||||||
let upper_main = Layout::default()
|
let upper_main = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Max(fd.height), Constraint::Min(1)].as_ref())
|
.constraints([Constraint::Max(fd.height), Constraint::Min(1)].as_ref())
|
||||||
@@ -306,6 +311,11 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
|
|
||||||
draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state);
|
draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state);
|
||||||
|
|
||||||
|
// Draw filter bar
|
||||||
|
if let Some(rect) = whole_layout.get(2) {
|
||||||
|
draw_blocks::filter_bar(*rect, f, app_data);
|
||||||
|
}
|
||||||
|
|
||||||
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(
|
||||||
|| {
|
|| {
|
||||||
@@ -320,8 +330,8 @@ 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 let Some(rect) = top_panel.get(1) {
|
||||||
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
|
draw_blocks::commands(app_data, *rect, f, &fd, gui_state);
|
||||||
|
|
||||||
// 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)
|
// 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 max_lens = app_data.lock().get_longest_port();
|
||||||
|
|||||||
Reference in New Issue
Block a user