Implement structure tab (#9)

* implement structure tab

* fix border colors

* remove margin
pull/10/head
Takayuki Maeda 3 years ago committed by GitHub
parent b8c36b5f94
commit 8ca65cafa4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

20
Cargo.lock generated

@ -476,6 +476,8 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum",
"strum_macros",
"tokio",
"toml",
"tui",
@ -1345,6 +1347,24 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "strum"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
[[package]]
name = "strum_macros"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "subtle"
version = "2.4.0"

@ -22,3 +22,5 @@ serde_json = "1.0"
serde = "1.0"
toml = "0.4"
regex = "1"
strum = "0.21"
strum_macros = "0.21"

@ -3,9 +3,29 @@ use crate::{
utils::get_tables,
};
use sqlx::mysql::MySqlPool;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use tui::widgets::{ListState, TableState};
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, EnumIter)]
pub enum Tab {
Records,
Structure,
}
impl std::fmt::Display for Tab {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl Tab {
pub fn names() -> Vec<String> {
Self::iter().map(|tab| tab.to_string()).collect()
}
}
pub enum FocusBlock {
DabataseList(bool),
TableList(bool),
@ -32,6 +52,18 @@ pub struct Table {
pub engine: Option<String>,
}
#[derive(sqlx::FromRow, Debug, Clone)]
pub struct Column {
#[sqlx(rename = "Field")]
pub field: String,
#[sqlx(rename = "Type")]
pub r#type: String,
#[sqlx(rename = "Collation")]
pub collation: String,
#[sqlx(rename = "Null")]
pub null: String,
}
pub struct RecordTable {
pub state: TableState,
pub headers: Vec<String>,
@ -90,6 +122,24 @@ impl RecordTable {
self.column_index -= 1
}
}
pub fn headers(&self) -> Vec<String> {
let mut headers = self.headers[self.column_index..].to_vec();
headers.insert(0, "".to_string());
headers
}
pub fn rows(&self) -> Vec<Vec<String>> {
let mut rows = self
.rows
.iter()
.map(|row| row[self.column_index..].to_vec())
.collect::<Vec<Vec<String>>>();
for (index, row) in rows.iter_mut().enumerate() {
row.insert(0, (index + 1).to_string())
}
rows
}
}
impl Database {
@ -107,7 +157,9 @@ pub struct App {
pub query: String,
pub databases: Vec<Database>,
pub record_table: RecordTable,
pub structure_table: RecordTable,
pub focus_block: FocusBlock,
pub selected_tab: Tab,
pub user_config: Option<UserConfig>,
pub selected_connection: ListState,
pub selected_database: ListState,
@ -124,7 +176,9 @@ impl Default for App {
query: String::new(),
databases: Vec::new(),
record_table: RecordTable::default(),
structure_table: RecordTable::default(),
focus_block: FocusBlock::DabataseList(false),
selected_tab: Tab::Records,
user_config: None,
selected_connection: ListState::default(),
selected_database: ListState::default(),
@ -136,6 +190,20 @@ impl Default for App {
}
impl App {
pub fn next_tab(&mut self) {
self.selected_tab = match self.selected_tab {
Tab::Records => Tab::Structure,
Tab::Structure => Tab::Records,
}
}
pub fn previous_tab(&mut self) {
self.selected_tab = match self.selected_tab {
Tab::Records => Tab::Structure,
Tab::Structure => Tab::Records,
}
}
pub fn next_table(&mut self) {
let i = match self.selected_table.selected() {
Some(i) => {

@ -29,6 +29,8 @@ pub async fn handle_app(key: Key, app: &mut App) -> anyhow::Result<()> {
_ => app.focus_block = FocusBlock::RecordTable(true),
},
Key::Char('e') => app.focus_block = FocusBlock::Query(true),
Key::Right => app.next_tab(),
Key::Left => app.previous_tab(),
Key::Esc => app.error = None,
_ => (),
}

@ -1,14 +1,14 @@
use crate::app::{App, FocusBlock};
use crate::event::Key;
use crate::utils::get_records;
use crate::utils::{get_columns, get_records};
pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<()> {
if focused {
match key {
Key::Char('j') => {
if app.selected_database.selected().is_some() {
app.record_table.column_index = 0;
app.next_table();
app.record_table.column_index = 0;
if let Some(database) = app.selected_database() {
if let Some(table) = app.selected_table() {
let (headers, records) =
@ -18,12 +18,23 @@ pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<(
app.record_table.rows = records;
}
}
app.structure_table.column_index = 0;
if let Some(database) = app.selected_database() {
if let Some(table) = app.selected_table() {
let (headers, records) =
get_columns(database, table, app.pool.as_ref().unwrap()).await?;
app.structure_table.state.select(Some(0));
app.structure_table.headers = headers;
app.structure_table.rows = records;
}
}
}
}
Key::Char('k') => {
if app.selected_database.selected().is_some() {
app.record_table.column_index = 0;
app.previous_table();
app.record_table.column_index = 0;
if let Some(database) = app.selected_database() {
if let Some(table) = app.selected_table() {
let (headers, records) =
@ -33,6 +44,17 @@ pub async fn handler(key: Key, app: &mut App, focused: bool) -> anyhow::Result<(
app.record_table.rows = records;
}
}
app.structure_table.column_index = 0;
if let Some(database) = app.selected_database() {
if let Some(table) = app.selected_table() {
let (headers, records) =
get_columns(database, table, app.pool.as_ref().unwrap()).await?;
app.structure_table.state.select(Some(0));
app.structure_table.headers = headers;
app.structure_table.rows = records;
}
}
}
}
Key::Esc => app.focus_block = FocusBlock::TableList(false),

@ -1,10 +1,10 @@
use crate::app::{App, FocusBlock};
use crate::app::{App, FocusBlock, Tab};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::{Block, Borders, Cell, Clear, List, ListItem, Paragraph, Row, Table},
widgets::{Block, Borders, Cell, Clear, List, ListItem, Paragraph, Row, Table, Tabs},
Frame,
};
use unicode_width::UnicodeWidthStr;
@ -26,7 +26,7 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_block {
FocusBlock::ConnectionList => Style::default().fg(Color::Green),
_ => Style::default(),
_ => Style::default().fg(Color::DarkGray),
});
let popup_layout = Layout::default()
.direction(Direction::Vertical)
@ -57,7 +57,6 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
}
let main_chunks = Layout::default()
.margin(2)
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(15), Constraint::Percentage(85)])
.split(f.size());
@ -84,9 +83,9 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.block(Block::default().borders(Borders::ALL).title("Databases"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_block {
FocusBlock::DabataseList(false) => Style::default().fg(Color::Magenta),
FocusBlock::DabataseList(false) => Style::default(),
FocusBlock::DabataseList(true) => Style::default().fg(Color::Green),
_ => Style::default(),
_ => Style::default().fg(Color::DarkGray),
});
f.render_stateful_widget(tasks, left_chunks[0], &mut app.selected_database);
@ -103,9 +102,9 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.block(Block::default().borders(Borders::ALL).title("Tables"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_block {
FocusBlock::TableList(false) => Style::default().fg(Color::Magenta),
FocusBlock::TableList(false) => Style::default(),
FocusBlock::TableList(true) => Style::default().fg(Color::Green),
_ => Style::default(),
_ => Style::default().fg(Color::DarkGray),
});
f.render_stateful_widget(tasks, left_chunks[1], &mut app.selected_table);
@ -124,36 +123,71 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
let right_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Length(5)].as_ref())
.constraints(
[
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(5),
]
.as_ref(),
)
.split(main_chunks[1]);
let titles = Tab::names().iter().cloned().map(Spans::from).collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL))
.select(app.selected_tab as usize)
.style(Style::default().fg(Color::DarkGray))
.highlight_style(
Style::default()
.fg(Color::Reset)
.add_modifier(Modifier::UNDERLINED),
);
f.render_widget(tabs, right_chunks[0]);
let query = Paragraph::new(app.input.as_ref())
.style(match app.focus_block {
FocusBlock::Query(true) => Style::default().fg(Color::Green),
FocusBlock::Query(false) => Style::default().fg(Color::Magenta),
_ => Style::default(),
FocusBlock::Query(false) => Style::default(),
_ => Style::default().fg(Color::DarkGray),
})
.block(Block::default().borders(Borders::ALL).title("Query"));
f.render_widget(query, right_chunks[0]);
f.render_widget(query, right_chunks[1]);
if let FocusBlock::Query(true) = app.focus_block {
f.set_cursor(
right_chunks[0].x + app.input.width() as u16 + 1 - app.input_cursor_x,
right_chunks[0].y + 1,
right_chunks[1].x + app.input.width() as u16 + 1 - app.input_cursor_x,
right_chunks[1].y + 1,
)
}
match app.selected_tab {
Tab::Records => draw_records_table(f, app, right_chunks[2])?,
Tab::Structure => draw_structure_table(f, app, right_chunks[2])?,
}
if let Some(err) = app.error.clone() {
draw_error_popup(f, err)?;
}
Ok(())
}
let header_cells = app.record_table.headers[app.record_table.column_index..]
fn draw_structure_table<B: Backend>(
f: &mut Frame<'_, B>,
app: &mut App,
layout_chunk: Rect,
) -> anyhow::Result<()> {
let headers = app.structure_table.headers();
let header_cells = headers
.iter()
.map(|h| Cell::from(h.to_string()).style(Style::default().fg(Color::White)));
let header = Row::new(header_cells).height(1).bottom_margin(1);
let rows = app.record_table.rows.iter().map(|item| {
let height = item[app.record_table.column_index..]
let rows = app.structure_table.rows();
let rows = rows.iter().map(|item| {
let height = item
.iter()
.map(|content| content.chars().filter(|c| *c == '\n').count())
.max()
.unwrap_or(0)
+ 1;
let cells = item[app.record_table.column_index..]
let cells = item
.iter()
.map(|c| Cell::from(c.to_string()).style(Style::default().fg(Color::White)));
Row::new(cells).height(height as u16).bottom_margin(1)
@ -163,19 +197,55 @@ pub fn draw<B: Backend>(f: &mut Frame<'_, B>, app: &mut App) -> anyhow::Result<(
.collect::<Vec<Constraint>>();
let t = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Records"))
.block(Block::default().borders(Borders::ALL).title("Structure"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_block {
FocusBlock::RecordTable(false) => Style::default().fg(Color::Magenta),
FocusBlock::RecordTable(false) => Style::default(),
FocusBlock::RecordTable(true) => Style::default().fg(Color::Green),
_ => Style::default(),
_ => Style::default().fg(Color::DarkGray),
})
.widths(&widths);
f.render_stateful_widget(t, right_chunks[1], &mut app.record_table.state);
f.render_stateful_widget(t, layout_chunk, &mut app.structure_table.state);
Ok(())
}
if let Some(err) = app.error.clone() {
draw_error_popup(f, err)?;
}
fn draw_records_table<B: Backend>(
f: &mut Frame<'_, B>,
app: &mut App,
layout_chunk: Rect,
) -> anyhow::Result<()> {
let headers = app.record_table.headers();
let header_cells = headers
.iter()
.map(|h| Cell::from(h.to_string()).style(Style::default().fg(Color::White)));
let header = Row::new(header_cells).height(1).bottom_margin(1);
let rows = app.record_table.rows();
let rows = rows.iter().map(|item| {
let height = item
.iter()
.map(|content| content.chars().filter(|c| *c == '\n').count())
.max()
.unwrap_or(0)
+ 1;
let cells = item
.iter()
.map(|c| Cell::from(c.to_string()).style(Style::default().fg(Color::White)));
Row::new(cells).height(height as u16).bottom_margin(1)
});
let widths = (0..10)
.map(|_| Constraint::Percentage(10))
.collect::<Vec<Constraint>>();
let t = Table::new(rows)
.header(header)
.block(Block::default().borders(Borders::ALL).title("Records"))
.highlight_style(Style::default().fg(Color::Green))
.style(match app.focus_block {
FocusBlock::RecordTable(false) => Style::default(),
FocusBlock::RecordTable(true) => Style::default().fg(Color::Green),
_ => Style::default().fg(Color::DarkGray),
})
.widths(&widths);
f.render_stateful_widget(t, layout_chunk, &mut app.record_table.state);
Ok(())
}

@ -2,7 +2,7 @@ use crate::app::{Database, Table};
use chrono::NaiveDate;
use futures::TryStreamExt;
use sqlx::mysql::{MySqlColumn, MySqlPool, MySqlRow};
use sqlx::{Column, Row, TypeInfo};
use sqlx::{Column as _, Row, TypeInfo};
pub async fn get_databases(pool: &MySqlPool) -> anyhow::Result<Vec<Database>> {
let databases = sqlx::query("SHOW DATABASES")
@ -52,6 +52,34 @@ pub async fn get_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()
.iter()
.map(|col| convert_column_value_to_string(&row, col))
.collect::<Vec<String>>(),
)
}
Ok((headers, records))
}
pub fn convert_column_value_to_string(row: &MySqlRow, column: &MySqlColumn) -> String {
let column_name = column.name();
match column.type_info().clone().name() {

Loading…
Cancel
Save