refactor components event

pull/24/head
Takayuki Maeda 3 years ago
parent 72fb787038
commit 6a166d8e34

@ -1,5 +1,9 @@
use crate::clipboard::Clipboard;
use crate::components::Component as _;
use crate::components::DrawableComponent as _;
use crate::components::EventState;
use crate::event::Key;
use crate::utils::{MySqlPool, Pool};
use crate::{
components::tab::Tab,
components::{
@ -8,7 +12,7 @@ use crate::{
},
user_config::UserConfig,
};
use sqlx::MySqlPool;
use database_tree::Database;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
@ -32,7 +36,7 @@ pub struct App {
pub connections: ConnectionsComponent,
pub table_status: TableStatusComponent,
pub clipboard: Clipboard,
pub pool: Option<MySqlPool>,
pub pool: Option<Box<dyn Pool>>,
pub error: ErrorComponent,
}
@ -111,4 +115,201 @@ impl App {
self.error.draw(f, Rect::default(), false)?;
Ok(())
}
pub async fn event(&mut self, key: Key) -> anyhow::Result<()> {
if self.tab.event(key)?.is_consumed() {
return Ok(());
}
if let Key::Esc = key {
if self.error.error.is_some() {
self.error.error = None;
return Ok(());
}
}
if self.components_event(key).await?.is_consumed() {
return Ok(());
};
self.move_focus(key);
Ok(())
}
pub async fn components_event(&mut self, key: Key) -> anyhow::Result<EventState> {
match self.focus {
Focus::ConnectionList => {
if self.connections.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
}
if let Key::Enter = key {
self.record_table.reset();
if let Some(conn) = self.connections.selected_connection() {
if let Some(pool) = self.pool.as_ref() {
pool.close().await;
}
self.pool = Some(Box::new(
MySqlPool::new(conn.database_url().as_str()).await?,
));
let databases = match &conn.database {
Some(database) => vec![Database::new(
database.clone(),
self.pool
.as_ref()
.unwrap()
.get_tables(database.clone())
.await?,
)],
None => self.pool.as_ref().unwrap().get_databases().await?,
};
self.databases.update(databases.as_slice()).unwrap();
self.focus = Focus::DabataseList
}
return Ok(EventState::Consumed);
}
}
Focus::DabataseList => {
if self.databases.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
}
if matches!(key, Key::Enter) && self.databases.tree_focused() {
if let Some((table, database)) = self.databases.tree().selected_table() {
self.focus = Focus::Table;
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_records(&database, &table.name, 0, None)
.await?;
self.record_table = RecordTableComponent::new(records, headers);
self.record_table.set_table(table.name.to_string());
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_columns(&database, &table.name)
.await?;
self.structure_table = TableComponent::new(records, headers);
self.table_status
.update(self.record_table.len() as u64, table);
}
return Ok(EventState::Consumed);
}
}
Focus::Table => {
match self.tab.selected_tab {
Tab::Records => {
if self.record_table.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
};
if let Key::Char('y') = key {
if let Some(text) = self.record_table.table.selected_cell() {
self.clipboard.store(text)
}
}
if matches!(key, Key::Enter) && self.record_table.filter_focused() {
self.record_table.focus = crate::components::record_table::Focus::Table;
if let Some((table, database)) = self.databases.tree().selected_table()
{
let (headers, records) = self
.pool
.as_ref()
.unwrap()
.get_records(
&database.clone(),
&table.name,
0,
if self.record_table.filter.input.is_empty() {
None
} else {
Some(self.record_table.filter.input.to_string())
},
)
.await?;
self.record_table.update(records, headers);
}
}
if self.record_table.table.eod {
return Ok(EventState::Consumed);
}
if let Some(index) = self.record_table.table.state.selected() {
if index.saturating_add(1)
% crate::utils::RECORDS_LIMIT_PER_PAGE as usize
== 0
{
if let Some((table, database)) =
self.databases.tree().selected_table()
{
let (_, records) = self
.pool
.as_ref()
.unwrap()
.get_records(
&database.clone(),
&table.name,
index as u16,
if self.record_table.filter.input.is_empty() {
None
} else {
Some(self.record_table.filter.input.to_string())
},
)
.await?;
if !records.is_empty() {
self.record_table.table.rows.extend(records);
} else {
self.record_table.table.end()
}
}
}
};
return Ok(EventState::NotConsumed);
}
Tab::Structure => {
if self.structure_table.event(key)?.is_consumed() {
return Ok(EventState::Consumed);
};
if let Key::Char('y') = key {
if let Some(text) = self.structure_table.selected_cell() {
self.clipboard.store(text)
}
};
return Ok(EventState::Consumed);
}
};
}
}
Ok(EventState::NotConsumed)
}
pub fn move_focus(&mut self, key: Key) {
if let Key::Char('c') = key {
self.focus = Focus::ConnectionList;
return;
}
match self.focus {
Focus::ConnectionList => {
if let Key::Enter = key {
self.focus = Focus::DabataseList
}
}
Focus::DabataseList => match key {
Key::Right if self.databases.tree_focused() => self.focus = Focus::Table,
_ => (),
},
Focus::Table => match key {
Key::Left => self.focus = Focus::DabataseList,
_ => (),
},
}
}
}

@ -1,10 +1,10 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use crate::user_config::Connection;
use anyhow::Result;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
layout::Rect,
style::{Color, Style},
text::{Span, Spans},
widgets::{Block, Borders, Clear, List, ListItem, ListState},
@ -99,12 +99,18 @@ impl DrawableComponent for ConnectionsComponent {
}
impl Component for ConnectionsComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char('j') => self.next_connection(),
Key::Char('k') => self.previous_connection(),
Key::Char('j') => {
self.next_connection();
return Ok(EventState::Consumed);
}
Key::Char('k') => {
self.previous_connection();
return Ok(EventState::Consumed);
}
_ => (),
}
Ok(())
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,5 @@
use super::{utils::scroll_vertical::VerticalScroll, Component, DrawableComponent};
use super::{utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState};
use crate::components::RecordTableComponent;
use crate::event::Key;
use crate::ui::common_nav;
use crate::ui::scrolllist::draw_list_block;
@ -12,7 +13,7 @@ use tui::{
style::{Color, Style},
symbols::line::HORIZONTAL,
text::Span,
widgets::{Block, Borders, Paragraph},
widgets::{Block, Borders},
Frame,
};
use unicode_width::UnicodeWidthStr;
@ -34,6 +35,7 @@ pub struct DatabasesComponent {
pub scroll: VerticalScroll,
pub input: String,
pub input_cursor_x: u16,
pub record_table: RecordTableComponent,
pub focus_block: FocusBlock,
}
@ -45,12 +47,13 @@ impl DatabasesComponent {
scroll: VerticalScroll::new(),
input: String::new(),
input_cursor_x: 0,
record_table: RecordTableComponent::default(),
focus_block: FocusBlock::Tree,
}
}
pub fn update(&mut self, list: &[Database], collapsed: &BTreeSet<&String>) -> Result<()> {
self.tree = DatabaseTree::new(list, collapsed)?;
pub fn update(&mut self, list: &[Database]) -> Result<()> {
self.tree = DatabaseTree::new(list, &BTreeSet::new())?;
self.filterd_tree = None;
self.input = String::new();
self.input_cursor_x = 0;
@ -208,16 +211,28 @@ impl DrawableComponent for DatabasesComponent {
}
impl Component for DatabasesComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
if tree_nav(
if let Some(tree) = self.filterd_tree.as_mut() {
tree
} else {
&mut self.tree
},
key,
) {
return Ok(EventState::Consumed);
}
match key {
Key::Char('/') if matches!(self.focus_block, FocusBlock::Tree) => {
self.focus_block = FocusBlock::Filter
self.focus_block = FocusBlock::Filter;
return Ok(EventState::Consumed);
}
Key::Char(c) if matches!(self.focus_block, FocusBlock::Filter) => {
self.input.push(c);
self.filterd_tree = Some(self.tree.filter(self.input.clone()))
self.filterd_tree = Some(self.tree.filter(self.input.clone()));
return Ok(EventState::Consumed);
}
Key::Delete | Key::Backspace => {
Key::Delete | Key::Backspace if matches!(self.focus_block, FocusBlock::Filter) => {
if !self.input.is_empty() {
if self.input_cursor_x == 0 {
self.input.pop();
@ -233,29 +248,32 @@ impl Component for DatabasesComponent {
None
} else {
Some(self.tree.filter(self.input.clone()))
}
};
return Ok(EventState::Consumed);
}
}
Key::Left => self.decrement_input_cursor_x(),
Key::Right => self.increment_input_cursor_x(),
Key::Left if matches!(self.focus_block, FocusBlock::Filter) => {
self.decrement_input_cursor_x();
return Ok(EventState::Consumed);
}
Key::Right if matches!(self.focus_block, FocusBlock::Filter) => {
self.increment_input_cursor_x();
return Ok(EventState::Consumed);
}
Key::Enter if matches!(self.focus_block, FocusBlock::Filter) => {
self.focus_block = FocusBlock::Tree
self.focus_block = FocusBlock::Tree;
return Ok(EventState::Consumed);
}
key => tree_nav(
if let Some(tree) = self.filterd_tree.as_mut() {
tree
} else {
&mut self.tree
},
key,
),
_ => (),
}
Ok(())
Ok(EventState::NotConsumed)
}
}
fn tree_nav(tree: &mut DatabaseTree, key: Key) {
fn tree_nav(tree: &mut DatabaseTree, key: Key) -> bool {
if let Some(common_nav) = common_nav(key) {
tree.move_selection(common_nav);
tree.move_selection(common_nav)
} else {
false
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use tui::{
@ -49,7 +49,7 @@ impl DrawableComponent for ErrorComponent {
}
impl Component for ErrorComponent {
fn event(&mut self, _key: Key) -> Result<()> {
Ok(())
fn event(&mut self, _key: Key) -> Result<EventState> {
Ok(EventState::NotConsumed)
}
}

@ -41,6 +41,28 @@ pub enum Direction {
Down,
}
#[derive(PartialEq)]
pub enum EventState {
Consumed,
NotConsumed,
}
impl EventState {
pub fn is_consumed(&self) -> bool {
*self == Self::Consumed
}
}
impl From<bool> for EventState {
fn from(consumed: bool) -> Self {
if consumed {
Self::Consumed
} else {
Self::NotConsumed
}
}
}
pub trait DrawableComponent {
fn draw<B: Backend>(&mut self, f: &mut Frame<B>, rect: Rect, focused: bool) -> Result<()>;
}
@ -48,9 +70,7 @@ pub trait DrawableComponent {
/// base component trait
#[async_trait]
pub trait Component {
fn event(&mut self, key: crate::event::Key) -> Result<()>;
// async fn async_event(&mut self, key: crate::event::Key) -> Result<()>;
fn event(&mut self, key: crate::event::Key) -> Result<EventState>;
fn focused(&self) -> bool {
false

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::components::{TableComponent, TableFilterComponent};
use crate::event::Key;
use anyhow::Result;
@ -45,6 +45,7 @@ impl RecordTableComponent {
pub fn reset(&mut self) {
self.table = TableComponent::default();
if !self.table.rows.is_empty() {
self.table.state.select(None);
self.table.state.select(Some(0))
}
self.filter = TableFilterComponent::default();
@ -57,6 +58,10 @@ impl RecordTableComponent {
pub fn set_table(&mut self, table: String) {
self.filter.table = Some(table)
}
pub fn filter_focused(&self) -> bool {
matches!(self.focus, Focus::Filter)
}
}
impl DrawableComponent for RecordTableComponent {
@ -76,14 +81,16 @@ impl DrawableComponent for RecordTableComponent {
}
impl Component for RecordTableComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char('/') => self.focus = Focus::Filter,
Key::Enter if matches!(self.focus, Focus::Filter) => self.focus = Focus::Table,
key if matches!(self.focus, Focus::Filter) => self.filter.event(key)?,
key if matches!(self.focus, Focus::Table) => self.table.event(key)?,
Key::Char('/') => {
self.focus = Focus::Filter;
return Ok(EventState::Consumed);
}
key if matches!(self.focus, Focus::Filter) => return Ok(self.filter.event(key)?),
key if matches!(self.focus, Focus::Table) => return Ok(self.table.event(key)?),
_ => (),
}
Ok(())
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use strum::IntoEnumIterator;
@ -62,12 +62,17 @@ impl DrawableComponent for TabComponent {
}
impl Component for TabComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char('1') => self.selected_tab = Tab::Records,
Key::Char('2') => self.selected_tab = Tab::Structure,
_ => (),
Key::Char('1') => {
self.selected_tab = Tab::Records;
Ok(EventState::Consumed)
}
Key::Char('2') => {
self.selected_tab = Tab::Structure;
Ok(EventState::Consumed)
}
_ => Ok(EventState::NotConsumed),
}
Ok(())
}
}

@ -1,5 +1,6 @@
use super::{
utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, TableValueComponent,
utils::scroll_vertical::VerticalScroll, Component, DrawableComponent, EventState,
TableValueComponent,
};
use crate::event::Key;
use anyhow::Result;
@ -12,13 +13,6 @@ use tui::{
Frame,
};
pub const RECORDS_LIMIT_PER_PAGE: u8 = 200;
pub enum FocusBlock {
Table,
Filter,
}
pub struct TableComponent {
pub state: TableState,
pub headers: Vec<String>,
@ -49,6 +43,7 @@ impl TableComponent {
pub fn new(rows: Vec<Vec<String>>, headers: Vec<String>) -> Self {
let mut state = TableState::default();
if !rows.is_empty() {
state.select(None);
state.select(Some(0))
}
Self {
@ -59,17 +54,6 @@ impl TableComponent {
}
}
pub fn update(&mut self, headers: Vec<String>, rows: Vec<Vec<String>>) {
self.headers = headers;
self.rows = rows;
self.column_page = 0;
self.column_index = 1;
self.state.select(None);
if !self.rows.is_empty() {
self.state.select(Some(0));
}
}
pub fn end(&mut self) {
self.eod = true;
}
@ -262,20 +246,47 @@ impl DrawableComponent for TableComponent {
}
impl Component for TableComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char('h') => self.previous_column(),
Key::Char('j') => self.next(1),
Key::Ctrl('d') => self.next(10),
Key::Char('k') => self.previous(1),
Key::Ctrl('u') => self.previous(10),
Key::Char('g') => self.scroll_top(),
Key::Char('r') => self.select_entire_row = true,
Key::Shift('G') | Key::Shift('g') => self.scroll_bottom(),
Key::Char('l') => self.next_column(),
Key::Char('h') => {
self.previous_column();
return Ok(EventState::Consumed);
}
Key::Char('j') => {
self.next(1);
return Ok(EventState::NotConsumed);
}
Key::Ctrl('d') => {
self.next(10);
return Ok(EventState::NotConsumed);
}
Key::Char('k') => {
self.previous(1);
return Ok(EventState::Consumed);
}
Key::Ctrl('u') => {
self.previous(10);
return Ok(EventState::Consumed);
}
Key::Char('g') => {
self.scroll_top();
return Ok(EventState::Consumed);
}
Key::Char('r') => {
self.select_entire_row = true;
return Ok(EventState::Consumed);
}
Key::Shift('G') | Key::Shift('g') => {
self.scroll_bottom();
return Ok(EventState::Consumed);
}
Key::Char('l') => {
self.next_column();
return Ok(EventState::Consumed);
}
_ => (),
}
Ok(())
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use tui::{
@ -86,25 +86,35 @@ impl DrawableComponent for TableFilterComponent {
}
impl Component for TableFilterComponent {
fn event(&mut self, key: Key) -> Result<()> {
fn event(&mut self, key: Key) -> Result<EventState> {
match key {
Key::Char(c) => self.input.push(c),
Key::Char(c) => {
self.input.push(c);
return Ok(EventState::Consumed);
}
Key::Delete | Key::Backspace => {
if self.input.width() > 0 {
if self.input_cursor_x == 0 {
self.input.pop();
return Ok(());
return Ok(EventState::Consumed);
}
if self.input.width() - self.input_cursor_x as usize > 0 {
self.input
.remove(self.input.width() - self.input_cursor_x as usize);
}
return Ok(EventState::Consumed);
}
}
Key::Left => self.decrement_input_cursor_x(),
Key::Right => self.increment_input_cursor_x(),
Key::Left => {
self.decrement_input_cursor_x();
return Ok(EventState::Consumed);
}
Key::Right => {
self.increment_input_cursor_x();
return Ok(EventState::Consumed);
}
_ => (),
}
Ok(())
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use database_tree::Table;
@ -85,7 +85,7 @@ impl DrawableComponent for TableStatusComponent {
}
impl Component for TableStatusComponent {
fn event(&mut self, _key: Key) -> Result<()> {
Ok(())
fn event(&mut self, _key: Key) -> Result<EventState> {
Ok(EventState::NotConsumed)
}
}

@ -1,4 +1,4 @@
use super::{Component, DrawableComponent};
use super::{Component, DrawableComponent, EventState};
use crate::event::Key;
use anyhow::Result;
use tui::{
@ -45,7 +45,7 @@ impl DrawableComponent for TableValueComponent {
}
impl Component for TableValueComponent {
fn event(&mut self, _key: Key) -> Result<()> {
fn event(&mut self, _key: Key) -> Result<EventState> {
todo!("scroll");
}
}

@ -1,46 +0,0 @@
use crate::app::{App, Focus};
use crate::components::Component as _;
use crate::event::Key;
use crate::utils::{get_databases, get_tables};
use database_tree::Database;
use sqlx::mysql::MySqlPool;
use std::collections::BTreeSet;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Enter => {
app.record_table.reset();
if let Some(conn) = app.connections.selected_connection() {
if let Some(pool) = app.pool.as_ref() {
pool.close().await;
}
let pool = MySqlPool::connect(conn.database_url().as_str()).await?;
app.pool = Some(pool);
app.focus = Focus::DabataseList;
}
if let Some(conn) = app.connections.selected_connection() {
match &conn.database {
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
.update(
get_databases(app.pool.as_ref().unwrap()).await?.as_slice(),
&BTreeSet::new(),
)
.unwrap(),
}
};
}
key => app.connections.event(key)?,
}
Ok(())
}

@ -1,48 +0,0 @@
use crate::app::{App, Focus};
use crate::components::table::RECORDS_LIMIT_PER_PAGE;
use crate::components::{Component as _, RecordTableComponent, TableComponent};
use crate::event::Key;
use crate::utils::{get_columns, get_records};
use database_tree::Database;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Char('c') if app.databases.tree_focused() => app.focus = Focus::ConnectionList,
Key::Right if app.databases.tree_focused() => app.focus = Focus::Table,
Key::Enter if app.databases.tree_focused() => {
if let Some((table, database)) = app.databases.tree().selected_table() {
app.focus = Focus::Table;
let (headers, records) = get_records(
&Database {
name: database.clone(),
tables: vec![],
},
&table,
0,
RECORDS_LIMIT_PER_PAGE,
None,
app.pool.as_ref().unwrap(),
)
.await?;
app.record_table = RecordTableComponent::new(records, headers);
app.record_table.set_table(table.name.to_string());
let (headers, records) = get_columns(
&Database {
name: database,
tables: vec![],
},
&table,
app.pool.as_ref().unwrap(),
)
.await?;
app.structure_table = TableComponent::new(records, headers);
app.table_status
.update(app.record_table.len() as u64, table);
}
}
key => app.databases.event(key)?,
}
Ok(())
}

@ -1,30 +0,0 @@
pub mod connection_list;
pub mod database_list;
pub mod record_table;
pub mod structure_table;
pub mod table_filter;
use crate::app::{App, Focus};
use crate::components::tab::Tab;
use crate::components::Component as _;
use crate::event::Key;
pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Esc if app.error.error.is_some() => {
app.error.error = None;
return Ok(());
}
key => app.tab.event(key)?,
}
match app.focus {
Focus::ConnectionList => connection_list::handler(key, app).await?,
Focus::DabataseList => database_list::handler(key, app).await?,
Focus::Table => match app.tab.selected_tab {
Tab::Records => record_table::handler(key, app).await?,
Tab::Structure => structure_table::handler(key, app).await?,
},
}
Ok(())
}

@ -1,60 +0,0 @@
use crate::app::{App, Focus};
use crate::components::table::RECORDS_LIMIT_PER_PAGE;
use crate::components::Component as _;
use crate::event::Key;
use crate::utils::get_records;
use database_tree::Database;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Left => app.focus = Focus::DabataseList,
Key::Char('c') => app.focus = Focus::ConnectionList,
Key::Char('y') => {
if let Some(text) = app.record_table.table.selected_cell() {
app.clipboard.store(text)
}
}
Key::Enter
if matches!(
app.record_table.focus,
crate::components::record_table::Focus::Filter
) =>
{
crate::handlers::table_filter::handler(key, app).await?
}
key => {
app.record_table.event(key)?;
if app.record_table.table.eod {
return Ok(());
}
if let Some(index) = app.record_table.table.state.selected() {
if index.saturating_add(1) % RECORDS_LIMIT_PER_PAGE as usize == 0 {
if let Some((table, database)) = app.databases.tree().selected_table() {
let (_, records) = get_records(
&Database {
name: database.clone(),
tables: vec![],
},
&table,
index as u16,
RECORDS_LIMIT_PER_PAGE,
if app.record_table.filter.input.is_empty() {
None
} else {
Some(app.record_table.filter.input.to_string())
},
app.pool.as_ref().unwrap(),
)
.await?;
if !records.is_empty() {
app.record_table.table.rows.extend(records);
} else {
app.record_table.table.end()
}
}
}
}
}
}
Ok(())
}

@ -1,17 +0,0 @@
use crate::app::{App, Focus};
use crate::components::Component as _;
use crate::event::Key;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Left => app.focus = Focus::DabataseList,
Key::Char('c') => app.focus = Focus::ConnectionList,
Key::Char('y') => {
if let Some(text) = app.structure_table.selected_cell() {
app.clipboard.store(text)
}
}
key => app.structure_table.event(key)?,
}
Ok(())
}

@ -1,36 +0,0 @@
use crate::app::App;
use crate::components::table::RECORDS_LIMIT_PER_PAGE;
use crate::event::Key;
use crate::utils::get_records;
use database_tree::Database;
pub async fn handler(key: Key, app: &mut App) -> anyhow::Result<()> {
match key {
Key::Enter => {
app.record_table.focus = crate::components::record_table::Focus::Table;
let filter_input = app.record_table.filter.input.to_string();
if app.record_table.filter.input.is_empty() {
return Ok(());
}
if let Some((table, database)) = app.databases.tree().selected_table() {
let (headers, records) = get_records(
&Database {
name: database.clone(),
tables: vec![],
},
&table,
0,
RECORDS_LIMIT_PER_PAGE,
Some(filter_input),
app.pool.as_ref().unwrap(),
)
.await?;
app.record_table.update(records, headers);
}
}
_ => (),
}
Ok(())
}

@ -2,7 +2,6 @@ mod app;
mod clipboard;
mod components;
mod event;
mod handlers;
mod ui;
mod user_config;
mod utils;
@ -12,7 +11,6 @@ mod log;
use crate::app::App;
use crate::event::{Event, Key};
use crate::handlers::handle_app;
use anyhow::Result;
use crossterm::{
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
@ -49,7 +47,7 @@ async fn main() -> anyhow::Result<()> {
if key == Key::Char('q') {
break;
};
match handle_app(key, &mut app).await {
match app.event(key).await {
Ok(_) => (),
Err(err) => app.error.set(err.to_string()),
}

@ -1,106 +1,150 @@
use async_trait::async_trait;
use chrono::NaiveDate;
use database_tree::{Database, Table};
use futures::TryStreamExt;
use sqlx::mysql::{MySqlColumn, MySqlPool, MySqlRow};
use sqlx::mysql::{MySqlColumn, MySqlPool as MPool, MySqlRow};
use sqlx::{Column as _, Row, TypeInfo};
pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SHOW DATABASES")
.fetch_all(pool)
.await?
.iter()
.map(|table| table.get(0))
.collect::<Vec<String>>();
let mut list = vec![];
for db in databases {
list.push(Database::new(
db.clone(),
get_tables(db.clone(), pool).await?,
))
}
Ok(list)
pub const RECORDS_LIMIT_PER_PAGE: u8 = 200;
#[async_trait]
pub trait Pool {
async fn get_databases(&self) -> anyhow::Result<Vec<Database>>;
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Table>>;
async fn get_records(
&self,
database: &String,
table: &String,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn get_columns(
&self,
database: &String,
table: &String,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)>;
async fn close(&self);
}
pub async fn get_tables(database: String, pool: &MySqlPool) -> anyhow::Result<Vec<Table>> {
let tables =
sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
.fetch_all(pool)
.await?;
Ok(tables)
pub struct MySqlPool {
pool: MPool,
}
pub async fn get_records(
database: &Database,
table: &Table,
page: u16,
limit: u8,
filter: Option<String>,
pool: &MySqlPool,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter {
format!(
"SELECT * FROM `{database}`.`{table}` WHERE {filter} LIMIT {page}, {limit}",
database = database.name,
table = table.name,
filter = filter,
page = page,
limit = limit
)
} else {
format!(
"SELECT * FROM `{}`.`{}` limit {page}, {limit}",
database.name,
table.name,
page = page,
limit = limit
)
};
let mut rows = sqlx::query(query.as_str()).fetch(pool);
let headers =
sqlx::query(format!("SHOW COLUMNS FROM `{}`.`{}`", database.name, table.name).as_str())
.fetch_all(pool)
impl MySqlPool {
pub async fn new(database_url: &str) -> anyhow::Result<Self> {
Ok(Self {
pool: MPool::connect(database_url).await?,
})
}
}
#[async_trait]
impl Pool for MySqlPool {
async fn get_databases(&self) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SHOW DATABASES")
.fetch_all(&self.pool)
.await?
.iter()
.map(|table| table.get(0))
.collect::<Vec<String>>();
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
records.push(
row.columns()
let mut list = vec![];
for db in databases {
list.push(Database::new(
db.clone(),
get_tables(db.clone(), &self.pool).await?,
))
}
Ok(list)
}
async fn get_tables(&self, database: String) -> anyhow::Result<Vec<Table>> {
let tables =
sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
.fetch_all(&self.pool)
.await?;
Ok(tables)
}
async fn get_records(
&self,
database: &String,
table: &String,
page: u16,
filter: Option<String>,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = if let Some(filter) = filter {
format!(
"SELECT * FROM `{database}`.`{table}` WHERE {filter} LIMIT {page}, {limit}",
database = database,
table = table,
filter = filter,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
} else {
format!(
"SELECT * FROM `{}`.`{}` limit {page}, {limit}",
database,
table,
page = page,
limit = RECORDS_LIMIT_PER_PAGE
)
};
let mut rows = sqlx::query(query.as_str()).fetch(&self.pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
.map(|column| column.name().to_string())
.collect();
records.push(
row.columns()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
}
Ok((headers, records))
}
Ok((headers, records))
}
pub async fn get_columns(
database: &Database,
table: &Table,
pool: &MySqlPool,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = format!(
"SHOW FULL COLUMNS FROM `{}`.`{}`",
database.name, table.name
);
let mut rows = sqlx::query(query.as_str()).fetch(pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|column| column.name().to_string())
.collect();
records.push(
row.columns()
async fn get_columns(
&self,
database: &String,
table: &String,
) -> anyhow::Result<(Vec<String>, Vec<Vec<String>>)> {
let query = format!("SHOW FULL COLUMNS FROM `{}`.`{}`", database, table);
let mut rows = sqlx::query(query.as_str()).fetch(&self.pool);
let mut headers = vec![];
let mut records = vec![];
while let Some(row) = rows.try_next().await? {
headers = row
.columns()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
.map(|column| column.name().to_string())
.collect();
records.push(
row.columns()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
}
Ok((headers, records))
}
Ok((headers, records))
async fn close(&self) {
self.pool.close().await;
}
}
pub async fn get_tables(database: String, pool: &MPool) -> anyhow::Result<Vec<Table>> {
let tables =
sqlx::query_as::<_, Table>(format!("SHOW TABLE STATUS FROM `{}`", database).as_str())
.fetch_all(pool)
.await?;
Ok(tables)
}
pub fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> String {

Loading…
Cancel
Save