Filter tables (#20)

* filter databases

* implement utils

* remove filter when input is empty

* remove focus shortcut

* add tests for databasetree

* fix clippy warnings

* fix placeholder
pull/22/head
Takayuki Maeda 3 years ago committed by GitHub
parent a9f918b53d
commit 4486fc707d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -44,6 +44,16 @@ impl DatabaseTree {
Ok(new_self)
}
pub fn filter(&self, filter_text: String) -> Self {
let mut new_self = Self {
items: self.items.filter(filter_text),
selection: Some(0),
visual_selection: None,
};
new_self.visual_selection = new_self.calc_visual_selection();
new_self
}
pub fn collapse_but_root(&mut self) {
self.items.collapse(0, true);
self.items.expand(0, false);
@ -282,3 +292,157 @@ impl DatabaseTree {
.unwrap_or_default()
}
}
#[cfg(test)]
mod test {
use crate::{Database, DatabaseTree, MoveSelection, Table};
// use pretty_assertions::assert_eq;
use std::collections::BTreeSet;
impl Table {
fn new(name: String) -> Self {
Table {
name,
create_time: None,
update_time: None,
engine: None,
}
}
}
#[test]
fn test_selection() {
let items = vec![Database::new(
"a".to_string(),
vec![Table::new("b".to_string())],
)];
// a
// b
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap();
assert!(tree.move_selection(MoveSelection::Right));
assert_eq!(tree.selection, Some(0));
assert!(tree.move_selection(MoveSelection::Down));
assert_eq!(tree.selection, Some(1));
}
#[test]
fn test_selection_skips_collapsed() {
let items = vec![
Database::new(
"a".to_string(),
vec![Table::new("b".to_string()), Table::new("c".to_string())],
),
Database::new("d".to_string(), vec![Table::new("e".to_string())]),
];
// a
// b
// c
// d
// e
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap();
tree.items.collapse(0, false);
tree.selection = Some(1);
assert!(tree.move_selection(MoveSelection::Down));
assert_eq!(tree.selection, Some(3));
}
#[test]
fn test_selection_left_collapse() {
let items = vec![Database::new(
"a".to_string(),
vec![Table::new("b".to_string()), Table::new("c".to_string())],
)];
// a
// b
// c
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap();
tree.selection = Some(0);
tree.items.expand(0, false);
assert!(tree.move_selection(MoveSelection::Left));
assert_eq!(tree.selection, Some(0));
assert!(tree.items.tree_items[0].kind().is_database_collapsed());
assert!(!tree.items.tree_items[1].info().is_visible());
assert!(!tree.items.tree_items[2].info().is_visible());
}
#[test]
fn test_selection_left_parent() {
let items = vec![Database::new(
"a".to_string(),
vec![Table::new("b".to_string()), Table::new("c".to_string())],
)];
// a
// b
// c
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap();
tree.selection = Some(2);
tree.items.expand(0, false);
assert!(tree.move_selection(MoveSelection::Left));
assert_eq!(tree.selection, Some(0));
}
#[test]
fn test_selection_right_expand() {
let items = vec![Database::new(
"a".to_string(),
vec![Table::new("b".to_string()), Table::new("c".to_string())],
)];
// a
// b
// c
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap();
tree.selection = Some(0);
assert!(tree.move_selection(MoveSelection::Right));
assert_eq!(tree.selection, Some(0));
assert!(!tree.items.tree_items[0].kind().is_database_collapsed());
assert!(tree.move_selection(MoveSelection::Right));
assert_eq!(tree.selection, Some(1));
}
#[test]
fn test_visible_selection() {
let items = vec![
Database::new(
"a".to_string(),
vec![Table::new("b".to_string()), Table::new("c".to_string())],
),
Database::new("d".to_string(), vec![Table::new("e".to_string())]),
];
// a
// b
// c
// d
// e
let mut tree = DatabaseTree::new(&items, &BTreeSet::new()).unwrap();
tree.items.expand(0, false);
tree.items.expand(3, false);
tree.selection = Some(0);
assert!(tree.move_selection(MoveSelection::Left));
assert!(tree.move_selection(MoveSelection::Down));
let s = tree.visual_selection().unwrap();
assert_eq!(s.count, 3);
assert_eq!(s.index, 1);
}
}

@ -19,6 +19,27 @@ impl DatabaseTreeItems {
})
}
pub fn filter(&self, filter_text: String) -> Self {
Self {
tree_items: self
.tree_items
.iter()
.filter(|item| item.is_database() || item.is_match(&filter_text))
.map(|item| {
if item.is_database() {
let mut item = item.clone();
item.set_collapsed(false);
item
} else {
let mut item = item.clone();
item.show();
item
}
})
.collect::<Vec<DatabaseTreeItem>>(),
}
}
fn create_items(
list: &[Database],
collapsed: &BTreeSet<&String>,

@ -98,6 +98,15 @@ impl DatabaseTreeItem {
})
}
pub fn set_collapsed(&mut self, collapsed: bool) {
if let DatabaseTreeItemKind::Database { name, .. } = self.kind() {
self.kind = DatabaseTreeItemKind::Database {
name: name.to_string(),
collapsed,
}
}
}
pub const fn info(&self) -> &TreeItemInfo {
&self.info
}
@ -128,9 +137,24 @@ impl DatabaseTreeItem {
}
}
pub fn show(&mut self) {
self.info.visible = true;
}
pub fn hide(&mut self) {
self.info.visible = false;
}
pub fn is_match(&self, filter_text: &str) -> bool {
match self.kind.clone() {
DatabaseTreeItemKind::Database { name, .. } => name.contains(filter_text),
DatabaseTreeItemKind::Table { table, .. } => table.name.contains(filter_text),
}
}
pub fn is_database(&self) -> bool {
self.kind.is_database()
}
}
impl Eq for DatabaseTreeItem {}

@ -3,16 +3,19 @@ use crate::event::Key;
use crate::ui::common_nav;
use crate::ui::scrolllist::draw_list_block;
use anyhow::Result;
use database_tree::{DatabaseTree, DatabaseTreeItem};
use database_tree::{Database, DatabaseTree, DatabaseTreeItem};
use std::collections::BTreeSet;
use std::convert::From;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
symbols::line::HORIZONTAL,
text::Span,
widgets::{Block, Borders},
Frame,
};
use unicode_width::UnicodeWidthStr;
// ▸
const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}";
@ -20,20 +23,61 @@ const FOLDER_ICON_COLLAPSED: &str = "\u{25b8}";
const FOLDER_ICON_EXPANDED: &str = "\u{25be}";
const EMPTY_STR: &str = "";
pub enum FocusBlock {
Filter,
Tree,
}
pub struct DatabasesComponent {
pub tree: DatabaseTree,
pub filterd_tree: Option<DatabaseTree>,
pub scroll: VerticalScroll,
pub input: String,
pub input_cursor_x: u16,
pub focus_block: FocusBlock,
}
impl DatabasesComponent {
pub fn new() -> Self {
Self {
tree: DatabaseTree::default(),
filterd_tree: None,
scroll: VerticalScroll::new(),
input: String::new(),
input_cursor_x: 0,
focus_block: FocusBlock::Tree,
}
}
pub fn update(&mut self, list: &[Database], collapsed: &BTreeSet<&String>) -> Result<()> {
self.tree = DatabaseTree::new(list, collapsed)?;
self.filterd_tree = None;
self.input = String::new();
self.input_cursor_x = 0;
Ok(())
}
pub fn tree_focused(&self) -> bool {
matches!(self.focus_block, FocusBlock::Tree)
}
pub fn tree(&self) -> &DatabaseTree {
self.filterd_tree.as_ref().unwrap_or(&self.tree)
}
pub fn increment_input_cursor_x(&mut self) {
if self.input_cursor_x > 0 {
self.input_cursor_x -= 1;
}
}
pub fn decrement_input_cursor_x(&mut self) {
if self.input_cursor_x < self.input.width() as u16 {
self.input_cursor_x += 1;
}
}
fn tree_item_to_span(item: &DatabaseTreeItem, selected: bool, width: u16) -> Span<'_> {
fn tree_item_to_span(item: DatabaseTreeItem, selected: bool, width: u16) -> Span<'static> {
let name = item.kind().name();
let indent = item.info().indent();
@ -72,21 +116,59 @@ impl DatabasesComponent {
}
fn draw_tree<B: Backend>(&self, f: &mut Frame<B>, area: Rect, focused: bool) {
let tree_height = usize::from(area.height.saturating_sub(2));
self.tree.visual_selection().map_or_else(
let tree_height = usize::from(area.height.saturating_sub(4));
let tree = if let Some(tree) = self.filterd_tree.as_ref() {
tree
} else {
&self.tree
};
tree.visual_selection().map_or_else(
|| {
self.scroll.reset();
},
|selection| {
self.scroll
.update(selection.index, selection.count, tree_height);
self.scroll.update(
selection.index,
selection.count.saturating_sub(2),
tree_height,
);
},
);
let items = self
.tree
let mut items = tree
.iterate(self.scroll.get_top(), tree_height)
.map(|(item, selected)| Self::tree_item_to_span(item, selected, area.width));
.map(|(item, selected)| Self::tree_item_to_span(item.clone(), selected, area.width))
.collect::<Vec<Span>>();
items.insert(
0,
Span::styled(
(0..area.width as usize)
.map(|_| HORIZONTAL)
.collect::<Vec<&str>>()
.join(""),
Style::default(),
),
);
items.insert(
0,
Span::styled(
format!(
"{}{:w$}",
if self.input.is_empty() && matches!(self.focus_block, FocusBlock::Tree) {
" / to filter tables".to_string()
} else {
self.input.clone()
},
w = area.width as usize
),
if let FocusBlock::Filter = self.focus_block {
Style::default()
} else {
Style::default().fg(Color::DarkGray)
},
),
);
let title = "Databases";
draw_list_block(
@ -101,39 +183,79 @@ impl DatabasesComponent {
})
.borders(Borders::ALL)
.border_style(Style::default()),
items,
items.into_iter(),
);
self.scroll.draw(f, area);
if let FocusBlock::Filter = self.focus_block {
f.set_cursor(
area.x + self.input.width() as u16 + 1 - self.input_cursor_x,
area.y + 1,
)
}
}
}
impl DrawableComponent for DatabasesComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, area: Rect, focused: bool) -> Result<()> {
if true {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(100)].as_ref())
.split(area);
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(100)].as_ref())
.split(area);
self.draw_tree(f, chunks[0], focused);
}
self.draw_tree(f, chunks[0], focused);
Ok(())
}
}
impl Component for DatabasesComponent {
fn event(&mut self, key: Key) -> Result<()> {
if tree_nav(&mut self.tree, key) {
return Ok(());
match key {
Key::Char('/') if matches!(self.focus_block, FocusBlock::Tree) => {
self.focus_block = FocusBlock::Filter
}
Key::Char(c) if matches!(self.focus_block, FocusBlock::Filter) => {
self.input.push(c);
self.filterd_tree = Some(self.tree.filter(self.input.clone()))
}
Key::Delete | Key::Backspace => {
if !self.input.is_empty() {
if self.input_cursor_x == 0 {
self.input.pop();
} else if self.input.width() - self.input_cursor_x as usize > 0 {
self.input.remove(
self.input
.width()
.saturating_sub(self.input_cursor_x as usize)
.saturating_sub(1),
);
}
self.filterd_tree = if self.input.is_empty() {
None
} else {
Some(self.tree.filter(self.input.clone()))
}
}
}
Key::Left => self.decrement_input_cursor_x(),
Key::Right => self.increment_input_cursor_x(),
Key::Enter if matches!(self.focus_block, FocusBlock::Filter) => {
self.focus_block = FocusBlock::Tree
}
key => tree_nav(
if let Some(tree) = self.filterd_tree.as_mut() {
tree
} else {
&mut self.tree
},
key,
),
}
Ok(())
}
}
fn tree_nav(tree: &mut DatabaseTree, key: Key) -> bool {
fn tree_nav(tree: &mut DatabaseTree, key: Key) {
if let Some(common_nav) = common_nav(key) {
tree.move_selection(common_nav)
} else {
false
tree.move_selection(common_nav);
}
}

@ -49,7 +49,7 @@ impl DrawableComponent for ErrorComponent {
}
impl Component for ErrorComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, _key: Key) -> Result<()> {
Ok(())
}
}

@ -2,7 +2,7 @@ use crate::app::{App, FocusBlock};
use crate::components::Component as _;
use crate::event::Key;
use crate::utils::{get_databases, get_tables};
use database_tree::{Database, DatabaseTree};
use database_tree::Database;
use sqlx::mysql::MySqlPool;
use std::collections::BTreeSet;
@ -21,23 +21,23 @@ pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
}
if let Some(conn) = app.connections.selected_connection() {
match &conn.database {
Some(database) => {
app.databases.tree = DatabaseTree::new(
Some(database) => app
.databases
.update(
&[Database::new(
database.clone(),
get_tables(database.clone(), app.pool.as_ref().unwrap()).await?,
)],
&BTreeSet::new(),
)
.unwrap()
}
None => {
app.databases.tree = DatabaseTree::new(
.unwrap(),
None => app
.databases
.update(
get_databases(app.pool.as_ref().unwrap()).await?.as_slice(),
&BTreeSet::new(),
)
.unwrap()
}
.unwrap(),
}
};
}

@ -6,11 +6,12 @@ use database_tree::Database;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Esc => app.focus_block = FocusBlock::DabataseList,
Key::Right => app.focus_block = FocusBlock::Table,
Key::Char('c') => app.focus_block = FocusBlock::ConnectionList,
Key::Enter => {
if let Some((table, database)) = app.databases.tree.selected_table() {
Key::Char('c') if app.databases.tree_focused() => {
app.focus_block = FocusBlock::ConnectionList
}
Key::Right if app.databases.tree_focused() => app.focus_block = FocusBlock::Table,
Key::Enter if app.databases.tree_focused() => {
if let Some((table, database)) = app.databases.tree().selected_table() {
app.focus_block = FocusBlock::Table;
let (headers, records) = get_records(
&Database {

@ -11,15 +11,7 @@ use crate::event::Key;
pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Char('d') => match app.focus_block {
FocusBlock::Query => (),
_ => app.focus_block = FocusBlock::DabataseList,
},
Key::Char('r') => match app.focus_block {
FocusBlock::Query => (),
_ => app.focus_block = FocusBlock::Table,
},
Key::Char('e') => app.focus_block = FocusBlock::Query,
Key::Ctrl('e') => app.focus_block = FocusBlock::Query,
Key::Esc if app.error.error.is_some() => {
app.error.error = None;
return Ok(());

Loading…
Cancel
Save