feat: filter containers, closes #37
Enable filtering of containers, toggled by pressing `F1` or `/`, build on PR #38 from MohammadShabaniSBU
This commit is contained in:
+186
-10
@@ -21,7 +21,7 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
gui_state::{BoxLocation, DeleteButton, Region},
|
||||
FrameData,
|
||||
FrameData, Status,
|
||||
};
|
||||
use super::{GuiState, SelectablePanel};
|
||||
|
||||
@@ -98,7 +98,7 @@ fn generate_block<'a>(
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.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
|
||||
@@ -233,7 +233,15 @@ pub fn containers(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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)
|
||||
.alignment(Alignment::Center);
|
||||
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
|
||||
/// TODO Should separate into loading icon/headers/help functions
|
||||
#[allow(clippy::too_many_lines)]
|
||||
@@ -521,6 +555,11 @@ pub fn heading_bar(
|
||||
.constraints(splits)
|
||||
.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 {
|
||||
let header_section_width = split_bar[1].width;
|
||||
|
||||
@@ -540,11 +579,11 @@ pub fn heading_bar(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// 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::Center);
|
||||
frame.render_widget(loading_paragraph, split_bar[0]);
|
||||
// // 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::Center);
|
||||
// frame.render_widget(loading_paragraph, split_bar[0]);
|
||||
|
||||
let container_splits = header_data.iter().map(|i| i.2).collect::<Vec<_>>();
|
||||
let headers_section = Layout::default()
|
||||
@@ -701,6 +740,13 @@ impl HelpInfo {
|
||||
"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(),
|
||||
@@ -2461,7 +2507,7 @@ mod tests {
|
||||
/// This will cause issues once the version has more than the current 5 chars (0.5.0)
|
||||
// Help popup is drawn correctly
|
||||
fn test_draw_blocks_help() {
|
||||
let (w, h) = (87, 32);
|
||||
let (w, h) = (87, 33);
|
||||
let mut setup = test_setup(w, h, true, true);
|
||||
|
||||
setup
|
||||
@@ -2492,6 +2538,7 @@ mod tests {
|
||||
" │ ( h ) toggle this help information │ ".to_owned(),
|
||||
" │ ( s ) save logs to file │ ".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(),
|
||||
" │ ( 1 - 9 ) sort by header - or click header │ ".to_owned(),
|
||||
" │ ( esc ) close dialog │ ".to_owned(),
|
||||
@@ -2502,6 +2549,7 @@ mod tests {
|
||||
" │ │ ".to_owned(),
|
||||
" │ │ ".to_owned(),
|
||||
" ╰───────────────────────────────────────────────────────────────────────────────────╯ ".to_owned(),
|
||||
" ".to_owned(),
|
||||
];
|
||||
|
||||
for (row_index, row) in expected.iter().enumerate() {
|
||||
@@ -3099,7 +3147,7 @@ mod tests {
|
||||
});
|
||||
|
||||
let expected = [
|
||||
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||
" name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ",
|
||||
"╭ 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 │",
|
||||
@@ -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]
|
||||
/// 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() {
|
||||
|
||||
+31
-28
@@ -163,19 +163,21 @@ pub enum Status {
|
||||
DockerConnect,
|
||||
Error,
|
||||
Exec,
|
||||
Filter,
|
||||
Help,
|
||||
Init,
|
||||
Logs,
|
||||
}
|
||||
|
||||
/// Global gui_state, stored in an Arc<Mutex>
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GuiState {
|
||||
delete_container: Option<ContainerId>,
|
||||
delete_map: HashMap<DeleteButton, Rect>,
|
||||
heading_map: HashMap<Header, Rect>,
|
||||
is_loading: HashSet<Uuid>,
|
||||
loading_handle: Option<JoinHandle<()>>,
|
||||
loading_index: u8,
|
||||
loading_set: HashSet<Uuid>,
|
||||
panel_map: HashMap<SelectablePanel, Rect>,
|
||||
selected_panel: SelectablePanel,
|
||||
status: HashSet<Status>,
|
||||
@@ -325,45 +327,46 @@ impl GuiState {
|
||||
} else {
|
||||
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 ' '
|
||||
pub fn get_loading(&self) -> char {
|
||||
if self.is_loading.is_empty() {
|
||||
' '
|
||||
} else {
|
||||
if self.is_loading() {
|
||||
FRAMES[usize::from(self.loading_index)]
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate the loading icon in its own Tokio thread
|
||||
pub fn start_loading_animation(
|
||||
gui_state: &Arc<Mutex<Self>>,
|
||||
loading_uuid: Uuid,
|
||||
) -> JoinHandle<()> {
|
||||
/// This should only be able to executed once, rather than multiple spawns
|
||||
pub fn start_loading_animation(gui_state: &Arc<Mutex<Self>>, loading_uuid: Uuid) {
|
||||
if !gui_state.lock().is_loading() {
|
||||
let inner_state = Arc::clone(gui_state);
|
||||
gui_state.lock().loading_handle = Some(tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
inner_state.lock().next_loading(loading_uuid);
|
||||
}
|
||||
}));
|
||||
}
|
||||
gui_state.lock().next_loading(loading_uuid);
|
||||
let gui_state = Arc::clone(gui_state);
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
gui_state.lock().next_loading(loading_uuid);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Stop the loading_spin function, and reset gui loading status
|
||||
pub fn stop_loading_animation(&mut self, handle: &JoinHandle<()>, loading_uuid: Uuid) {
|
||||
handle.abort();
|
||||
self.remove_loading(loading_uuid);
|
||||
pub fn stop_loading_animation(&mut self, loading_uuid: Uuid) {
|
||||
self.loading_set.remove(&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
|
||||
|
||||
+15
-5
@@ -64,7 +64,6 @@ impl Ui {
|
||||
is_running: Arc<AtomicBool>,
|
||||
) {
|
||||
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 mut ui = Self {
|
||||
app_data,
|
||||
@@ -264,14 +263,20 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
|
||||
/// 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>>) {
|
||||
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()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Max(1), Constraint::Min(1)].as_ref())
|
||||
.constraints(whole_constraints)
|
||||
.split(f.size());
|
||||
|
||||
// Split into 3, containers+controls, logs, then graphs
|
||||
// This one is the issue!
|
||||
let upper_main = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.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 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() {
|
||||
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
|
||||
if fd.has_containers {
|
||||
draw_blocks::commands(app_data, top_panel[1], f, &fd, gui_state);
|
||||
if let Some(rect) = top_panel.get(1) {
|
||||
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)
|
||||
let max_lens = app_data.lock().get_longest_port();
|
||||
|
||||
Reference in New Issue
Block a user