feat: add connection colors, query history, SQL autocomplete, and EXPLAIN visualizer
Add four developer/QA features: - Connection color coding: color picker in dialog, colored indicators across toolbar, tabs, status bar, and connection selectors - Query history: Rust backend with JSON file storage (500 entry cap), sidebar panel with search/clear, auto-recording from workspace - Schema-aware SQL autocomplete: backend fetches column metadata, CodeMirror receives schema namespace with public tables unprefixed - EXPLAIN ANALYZE visualizer: recursive tree view with cost-colored bars, expand/collapse nodes, buffers info, Results/Explain tab toggle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
76
src-tauri/src/commands/history.rs
Normal file
76
src-tauri/src/commands/history.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use crate::error::{TuskError, TuskResult};
|
||||
use crate::models::history::HistoryEntry;
|
||||
use std::fs;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
fn get_history_path(app: &AppHandle) -> TuskResult<std::path::PathBuf> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|e| TuskError::Custom(e.to_string()))?;
|
||||
fs::create_dir_all(&dir)?;
|
||||
Ok(dir.join("query_history.json"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_history_entry(app: AppHandle, entry: HistoryEntry) -> TuskResult<()> {
|
||||
let path = get_history_path(&app)?;
|
||||
let mut entries = if path.exists() {
|
||||
let data = fs::read_to_string(&path)?;
|
||||
serde_json::from_str::<Vec<HistoryEntry>>(&data).unwrap_or_default()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
entries.insert(0, entry);
|
||||
entries.truncate(500);
|
||||
|
||||
let data = serde_json::to_string_pretty(&entries)?;
|
||||
fs::write(&path, data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_history(
|
||||
app: AppHandle,
|
||||
connection_id: Option<String>,
|
||||
search: Option<String>,
|
||||
limit: Option<usize>,
|
||||
) -> TuskResult<Vec<HistoryEntry>> {
|
||||
let path = get_history_path(&app)?;
|
||||
if !path.exists() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let data = fs::read_to_string(&path)?;
|
||||
let entries: Vec<HistoryEntry> = serde_json::from_str(&data).unwrap_or_default();
|
||||
|
||||
let filtered: Vec<HistoryEntry> = entries
|
||||
.into_iter()
|
||||
.filter(|e| {
|
||||
if let Some(ref cid) = connection_id {
|
||||
if &e.connection_id != cid {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(ref s) = search {
|
||||
let lower = s.to_lowercase();
|
||||
if !e.sql.to_lowercase().contains(&lower) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.take(limit.unwrap_or(100))
|
||||
.collect();
|
||||
|
||||
Ok(filtered)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_history(app: AppHandle) -> TuskResult<()> {
|
||||
let path = get_history_path(&app)?;
|
||||
if path.exists() {
|
||||
fs::remove_file(&path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod connections;
|
||||
pub mod data;
|
||||
pub mod export;
|
||||
pub mod history;
|
||||
pub mod queries;
|
||||
pub mod schema;
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::error::{TuskError, TuskResult};
|
||||
use crate::models::schema::{ColumnInfo, ConstraintInfo, IndexInfo, SchemaObject};
|
||||
use crate::state::AppState;
|
||||
use sqlx::Row;
|
||||
use std::collections::HashMap;
|
||||
use tauri::State;
|
||||
|
||||
#[tauri::command]
|
||||
@@ -340,3 +341,40 @@ pub async fn get_table_indexes(
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_completion_schema(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
) -> TuskResult<HashMap<String, HashMap<String, Vec<String>>>> {
|
||||
let pools = state.pools.read().await;
|
||||
let pool = pools
|
||||
.get(&connection_id)
|
||||
.ok_or(TuskError::NotConnected(connection_id))?;
|
||||
|
||||
let rows = sqlx::query(
|
||||
"SELECT table_schema, table_name, column_name \
|
||||
FROM information_schema.columns \
|
||||
WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast') \
|
||||
ORDER BY table_schema, table_name, ordinal_position",
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(TuskError::Database)?;
|
||||
|
||||
let mut result: HashMap<String, HashMap<String, Vec<String>>> = HashMap::new();
|
||||
for row in &rows {
|
||||
let schema: String = row.get(0);
|
||||
let table: String = row.get(1);
|
||||
let column: String = row.get(2);
|
||||
|
||||
result
|
||||
.entry(schema)
|
||||
.or_default()
|
||||
.entry(table)
|
||||
.or_default()
|
||||
.push(column);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ pub fn run() {
|
||||
commands::schema::get_table_columns,
|
||||
commands::schema::get_table_constraints,
|
||||
commands::schema::get_table_indexes,
|
||||
commands::schema::get_completion_schema,
|
||||
// data
|
||||
commands::data::get_table_data,
|
||||
commands::data::update_row,
|
||||
@@ -42,6 +43,10 @@ pub fn run() {
|
||||
// export
|
||||
commands::export::export_csv,
|
||||
commands::export::export_json,
|
||||
// history
|
||||
commands::history::add_history_entry,
|
||||
commands::history::get_history,
|
||||
commands::history::clear_history,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
15
src-tauri/src/models/history.rs
Normal file
15
src-tauri/src/models/history.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HistoryEntry {
|
||||
pub id: String,
|
||||
pub connection_id: String,
|
||||
pub connection_name: String,
|
||||
pub database: String,
|
||||
pub sql: String,
|
||||
pub status: String,
|
||||
pub error_message: Option<String>,
|
||||
pub row_count: Option<i64>,
|
||||
pub execution_time_ms: f64,
|
||||
pub executed_at: String,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod connection;
|
||||
pub mod history;
|
||||
pub mod query_result;
|
||||
pub mod schema;
|
||||
|
||||
Reference in New Issue
Block a user