diff --git a/Cargo.toml b/Cargo.toml index 454e62f..4a40f20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "rust-sql-gui-ui" -version = "1.0.0-alpha.9" +name = "rsql" +version = "1.0.0-beta.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -leptos = { version = "0.6.8", features = ["csr", "nightly"] } +leptos = { version = "0.6.11", features = ["csr", "nightly"] } leptos_devtools = { git = "https://github.com/luoxiaozero/leptos-devtools" } serde = { version = "1.0.192", features = ["derive"] } serde-wasm-bindgen = "0.6.3" wasm-bindgen = { version ="0.2.91", features = ["serde-serialize"] } js-sys = "0.3.68" -leptos-use = { version = "0.10.9", features = ["serde", "serde_json"]} +leptos-use = { version = "0.10.10", features = ["serde", "serde_json"]} leptos_icons = "0.3.0" # https://carlosted.github.io/icondata/ serde_json = "1.0.113" wasm-bindgen-futures = "0.4.39" @@ -22,6 +22,16 @@ common = { path = "common" } futures = "0.3.30" async-stream = "0.3.5" icondata = "0.3.0" +ahash = { version = "0.8.11", features = ["serde"] } +leptos_toaster = { version = "0.1.6", features = ["builtin_toast"] } +proc-macro2 = "1.0.82" +quote = "1.0.36" +syn = { version = "2.0.64", features = ["full"] } +chrono = "0.4.38" + [workspace] members = ["src-tauri", "common"] + +[lib] +proc-macro = true diff --git a/common/Cargo.toml b/common/Cargo.toml index aad3e28..22b4532 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "common" -version = "1.0.0-alpha.9" +version = "1.0.0-beta.0" edition = "2021" [dependencies] diff --git a/common/src/drivers/mod.rs b/common/src/drivers/mod.rs deleted file mode 100644 index 6aa782e..0000000 --- a/common/src/drivers/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod postgresql; diff --git a/common/src/drivers/postgresql.rs b/common/src/drivers/postgresql.rs deleted file mode 100644 index 25f8e95..0000000 --- a/common/src/drivers/postgresql.rs +++ /dev/null @@ -1,20 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] -pub struct Postgresql { - pub user: String, - pub password: String, - pub host: String, - pub port: String, -} - -impl Postgresql { - pub fn new(user: String, password: String, host: String, port: String) -> Self { - Self { - user, - password, - host, - port, - } - } -} diff --git a/common/src/enums.rs b/common/src/enums.rs index 1ebb7e6..7b706df 100644 --- a/common/src/enums.rs +++ b/common/src/enums.rs @@ -2,22 +2,24 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; -use super::projects::postgresql::Postgresql; - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum Project { - POSTGRESQL(Postgresql), -} - -#[derive(Clone, Serialize, Deserialize)] +#[derive(Clone, Copy, Serialize, Deserialize, Default)] pub enum Drivers { - POSTGRESQL, + #[default] + PGSQL, } impl Display for Drivers { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Drivers::POSTGRESQL => write!(f, "POSTGRESQL"), + Drivers::PGSQL => write!(f, "PGSQL"), + } + } +} + +impl AsRef for Drivers { + fn as_ref(&self) -> &str { + match self { + Drivers::PGSQL => "PGSQL", } } } @@ -25,6 +27,43 @@ impl Display for Drivers { #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub enum ProjectConnectionStatus { Connected, + Connecting, #[default] Disconnected, + Failed, } + +impl std::error::Error for ProjectConnectionStatus {} +impl Display for ProjectConnectionStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProjectConnectionStatus::Connected => write!(f, "Connected"), + ProjectConnectionStatus::Connecting => write!(f, "Connecting"), + ProjectConnectionStatus::Disconnected => write!(f, "Disconnected"), + ProjectConnectionStatus::Failed => write!(f, "Failed"), + } + } +} + +use std::fmt; + +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +pub enum PostgresqlError { + ConnectionTimeout, + ConnectionError, + QueryTimeout, + QueryError, +} + +impl std::error::Error for PostgresqlError {} +impl fmt::Display for PostgresqlError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + PostgresqlError::ConnectionTimeout => write!(f, "ConnectionTimeout"), + PostgresqlError::ConnectionError => write!(f, "ConnectionError"), + PostgresqlError::QueryTimeout => write!(f, "QueryTimeout"), + PostgresqlError::QueryError => write!(f, "QueryError"), + } + } +} + diff --git a/common/src/lib.rs b/common/src/lib.rs index cdc608b..de9476e 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -1,3 +1,3 @@ -pub mod drivers; pub mod enums; -pub mod projects; +pub mod types; + diff --git a/common/src/projects/mod.rs b/common/src/projects/mod.rs deleted file mode 100644 index 6aa782e..0000000 --- a/common/src/projects/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod postgresql; diff --git a/common/src/projects/postgresql.rs b/common/src/projects/postgresql.rs deleted file mode 100644 index 222c24c..0000000 --- a/common/src/projects/postgresql.rs +++ /dev/null @@ -1,36 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; - -use crate::{drivers::postgresql::Postgresql as PostgresqlDriver, enums::ProjectConnectionStatus}; - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct Postgresql { - pub name: String, - pub driver: PostgresqlDriver, - pub schemas: Option>, - pub tables: Option>>, - pub connection_status: ProjectConnectionStatus, - pub relations: Option>, -} - -impl Default for Postgresql { - fn default() -> Self { - Self { - name: String::default(), - driver: PostgresqlDriver::default(), - schemas: None, - tables: None, - connection_status: ProjectConnectionStatus::Disconnected, - relations: None, - } - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct PostgresqlRelation { - pub constraint_name: String, - pub table_name: String, - pub column_name: String, - pub foreign_table_name: String, - pub foreign_column_name: String, -} diff --git a/common/src/types/mod.rs b/common/src/types/mod.rs new file mode 100644 index 0000000..173f9d2 --- /dev/null +++ b/common/src/types/mod.rs @@ -0,0 +1,2 @@ +pub mod pgsql; + diff --git a/common/src/types/pgsql.rs b/common/src/types/pgsql.rs new file mode 100644 index 0000000..dd0d390 --- /dev/null +++ b/common/src/types/pgsql.rs @@ -0,0 +1,4 @@ +pub type PgsqlLoadSchemas = Vec; +pub type PgsqlLoadTables = Vec<(String, String)>; +pub type PgsqlRunQuery = (Vec, Vec>, f32); + diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9409a8d..a570c3b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "rust-sql-gui" -version = "1.0.0-alpha.9" +name = "rsql_tauri" +version = "1.0.0-beta.0" description = "PostgreSQL GUI written in Rust" authors = ["Daniel Boros"] license = "" @@ -10,17 +10,21 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] -tauri-build = { version = "1.5.1", features = [] } +tauri-build = { version = "1.5.2", features = [] } [dependencies] common = { path = "../common" } -tauri = { version = "1.6.0", features = [ "shell-open", "fs-all"] } +tauri = { version = "1.6.5", features = ["shell-open", "fs-all"] } serde = { version = "1.0.193", features = ["derive"] } serde_json = "1.0.108" -tokio = "1.36.0" +tokio = "1.37.0" tokio-postgres = "0.7.10" chrono = "0.4.31" sled = "0.34.7" +anyhow = "1.0.83" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["fmt"] } +ahash = { version = "0.8.11", features = ["serde"] } diff --git a/src-tauri/src/dbs/project.rs b/src-tauri/src/dbs/project.rs index c309711..8eab5c3 100644 --- a/src-tauri/src/dbs/project.rs +++ b/src-tauri/src/dbs/project.rs @@ -1,80 +1,49 @@ -use common::{ - drivers::postgresql::Postgresql as PostgresqlDriver, - enums::{Drivers, Project}, - projects::postgresql::Postgresql, -}; +use std::collections::BTreeMap; + use tauri::{Result, State}; use crate::AppState; #[tauri::command(rename_all = "snake_case")] -pub async fn select_projects(app_state: State<'_, AppState>) -> Result> { +pub async fn project_db_select(app_state: State<'_, AppState>) -> Result> { let project_db = app_state.project_db.lock().await; - let mut projects = project_db - .clone() - .unwrap() - .iter() - .map(|r| { - let (project, connection_string) = r.unwrap(); - let project = String::from_utf8(project.to_vec()).unwrap(); - let connection_string = String::from_utf8(connection_string.to_vec()).unwrap(); - let connection_string = connection_string.split(':').collect::>(); - let _driver = connection_string[0].to_string(); - let _driver = _driver.split('=').collect::>()[1]; - let project_details = match _driver { - d if d == Drivers::POSTGRESQL.to_string() => { - let mut driver = PostgresqlDriver::default(); + let db = project_db.clone().unwrap(); + let mut projects = BTreeMap::new(); - for c in connection_string[1..].iter() { - let c = c.split('=').collect::>(); - let key = c[0]; - let value = c[1]; + if db.is_empty() { + tracing::info!("No projects found in the database"); + return Ok(projects); + } - match key { - "user" => driver.user = value.to_string(), - "password" => driver.password = value.to_string(), - "host" => driver.host = value.to_string(), - "port" => driver.port = value.to_string(), - _ => (), - } - } + for p in db.iter() { + let project = p.unwrap(); - Project::POSTGRESQL(Postgresql { - name: project.clone(), - driver, - ..Postgresql::default() - }) - } - _ => unreachable!(), - }; - (project, project_details) - }) - .collect::>(); - projects.sort_by(|a, b| a.0.cmp(&b.0)); + let project = ( + String::from_utf8(project.0.to_vec()).unwrap(), + String::from_utf8(project.1.to_vec()).unwrap(), + ); + projects.insert(project.0, project.1); + } Ok(projects) } #[tauri::command(rename_all = "snake_case")] -pub async fn insert_project(project: Project, app_state: State<'_, AppState>) -> Result { +pub async fn project_db_insert( + project_id: &str, + project_details: &str, + app_state: State<'_, AppState>, +) -> Result<()> { let project_db = app_state.project_db.lock().await; let db = project_db.clone().unwrap(); - match project { - Project::POSTGRESQL(project) => { - let driver = &project.driver; - let connection_string = format!( - "driver=POSTGRESQL:user={}:password={}:host={}:port={}", - driver.user, driver.password, driver.host, driver.port, - ); - db.insert(&project.name, &*connection_string).unwrap(); - Ok(Project::POSTGRESQL(project)) - } - } + db.insert(project_id, project_details).unwrap(); + Ok(()) } #[tauri::command(rename_all = "snake_case")] -pub async fn delete_project(project_name: &str, app_state: State<'_, AppState>) -> Result<()> { +pub async fn project_db_delete(project_id: &str, app_state: State<'_, AppState>) -> Result<()> { let db = app_state.project_db.lock().await; let db = db.clone().unwrap(); - db.remove(project_name).unwrap(); + db.remove(project_id).unwrap(); Ok(()) } + diff --git a/src-tauri/src/dbs/query.rs b/src-tauri/src/dbs/query.rs index fa2ca8f..b0ceb92 100644 --- a/src-tauri/src/dbs/query.rs +++ b/src-tauri/src/dbs/query.rs @@ -5,17 +5,7 @@ use tauri::{AppHandle, Manager, Result, State}; use crate::AppState; #[tauri::command(rename_all = "snake_case")] -pub async fn insert_query(key: &str, sql: &str, app: AppHandle) -> Result<()> { - let app_state = app.state::(); - let db = app_state.query_db.lock().await; - if let Some(ref db_instance) = *db { - db_instance.insert(key, sql).unwrap(); - } - Ok(()) -} - -#[tauri::command(rename_all = "snake_case")] -pub async fn select_queries(app_state: State<'_, AppState>) -> Result> { +pub async fn query_db_select(app_state: State<'_, AppState>) -> Result> { let query_db = app_state.query_db.lock().await; let mut queries = BTreeMap::new(); if let Some(ref query_db) = *query_db { @@ -30,10 +20,21 @@ pub async fn select_queries(app_state: State<'_, AppState>) -> Result) -> Result<()> { +pub async fn query_db_insert(query_id: &str, sql: &str, app: AppHandle) -> Result<()> { + let app_state = app.state::(); + let db = app_state.query_db.lock().await; + if let Some(ref db_instance) = *db { + db_instance.insert(query_id, sql).unwrap(); + } + Ok(()) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn query_db_delete(query_id: &str, app_state: State<'_, AppState>) -> Result<()> { let query_db = app_state.query_db.lock().await; if let Some(ref query_db) = *query_db { - query_db.remove(key).unwrap(); + query_db.remove(query_id).unwrap(); }; Ok(()) } + diff --git a/src-tauri/src/drivers/mod.rs b/src-tauri/src/drivers/mod.rs index 6aa782e..173f9d2 100644 --- a/src-tauri/src/drivers/mod.rs +++ b/src-tauri/src/drivers/mod.rs @@ -1 +1,2 @@ -pub mod postgresql; +pub mod pgsql; + diff --git a/src-tauri/src/drivers/pgsql.rs b/src-tauri/src/drivers/pgsql.rs new file mode 100644 index 0000000..6c48f7d --- /dev/null +++ b/src-tauri/src/drivers/pgsql.rs @@ -0,0 +1,258 @@ +use std::{sync::Arc, time::Instant}; + +use common::{ + enums::{PostgresqlError, ProjectConnectionStatus}, + types::pgsql::{PgsqlLoadSchemas, PgsqlLoadTables}, +}; +use tauri::{AppHandle, Manager, Result, State}; +use tokio::{sync::Mutex, time as tokio_time}; +use tokio_postgres::{connect, NoTls}; + +use crate::{utils::reflective_get, AppState}; + +#[tauri::command(rename_all = "snake_case")] +pub async fn pgsql_connector( + project_id: &str, + key: Option<&str>, + app: AppHandle, +) -> Result { + let app_state = app.state::(); + let mut clients = app_state.client.lock().await; + + // check if connection already exists + if clients.as_ref().unwrap().contains_key(project_id) { + tracing::info!("Postgres connection already exists!"); + return Ok(ProjectConnectionStatus::Connected); + } + + let key = match key { + Some(key) => key.to_string(), + None => { + let projects_db = app_state.project_db.lock().await; + let projects_db = projects_db.as_ref().unwrap(); + let project_details = projects_db.get(project_id).unwrap().unwrap().to_vec(); + let project_details = String::from_utf8(project_details).unwrap(); + let project_details = project_details.split(":").collect::>(); + let project_details = project_details + .into_iter() + .skip(1) + .map(|s| { + let kv = s.split('=').collect::>(); + kv[1].to_owned() + }) + .collect::>(); + let project_details = format!( + "user={} password={} host={} port={}", + project_details[0], project_details[1], project_details[2], project_details[3] + ); + project_details + } + }; + + let connection = tokio_time::timeout(tokio_time::Duration::from_secs(10), connect(&key, NoTls)) + .await + .map_err(|_| PostgresqlError::ConnectionTimeout); + + if connection.is_err() { + tracing::error!("Postgres connection timeout error!"); + return Ok(ProjectConnectionStatus::Failed); + } + + let connection = connection.unwrap(); + if connection.is_err() { + tracing::error!("Postgres connection error!"); + return Ok(ProjectConnectionStatus::Failed); + } + + let is_connection_error = Arc::new(Mutex::new(false)); + let (client, connection) = connection.unwrap(); + tracing::info!("Postgres connection established!"); + + // check if connection has some error + tokio::spawn({ + let is_connection_error = Arc::clone(&is_connection_error); + async move { + if let Err(e) = connection.await { + tracing::info!("Postgres connection error: {:?}", e); + *is_connection_error.lock().await = true; + } + } + }); + + if *is_connection_error.lock().await { + tracing::error!("Postgres connection error!"); + return Ok(ProjectConnectionStatus::Failed); + } + + let clients = clients.as_mut().unwrap(); + clients.insert(project_id.to_string(), client); + + Ok(ProjectConnectionStatus::Connected) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn pgsql_load_schemas( + project_id: &str, + app_state: State<'_, AppState>, +) -> Result { + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project_id).unwrap(); + + let query = tokio_time::timeout( + tokio_time::Duration::from_secs(10), + client.query( + r#" + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name NOT IN ('pg_catalog', 'information_schema') + ORDER BY schema_name; + "#, + &[], + ), + ) + .await + .map_err(|_| PostgresqlError::QueryTimeout); + + if query.is_err() { + tracing::error!("Postgres schema query timeout error!"); + return Err(tauri::Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + PostgresqlError::QueryTimeout, + ))); + } + + let query = query.unwrap(); + if query.is_err() { + tracing::error!("Postgres schema query error!"); + return Err(tauri::Error::Io(std::io::Error::new( + std::io::ErrorKind::Other, + PostgresqlError::QueryError, + ))); + } + + let qeury = query.unwrap(); + let schemas = qeury.iter().map(|r| r.get(0)).collect::>(); + tracing::info!("Postgres schemas: {:?}", schemas); + Ok(schemas) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn pgsql_load_tables( + project_id: &str, + schema: &str, + app_state: State<'_, AppState>, +) -> Result { + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project_id).unwrap(); + let query = client + .query( + r#"--sql + SELECT + table_name, + pg_size_pretty(pg_total_relation_size('"' || table_schema || '"."' || table_name || '"')) AS size + FROM + information_schema.tables + WHERE + table_schema = $1 + ORDER BY + table_name; + "#, + &[&schema], + ) + .await + .unwrap(); + let tables = query + .iter() + .map(|r| (r.get(0), r.get(1))) + .collect::>(); + Ok(tables) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn pgsql_run_query( + project_id: &str, + sql: &str, + app_state: State<'_, AppState>, +) -> Result<(Vec, Vec>, f32)> { + let start = Instant::now(); + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project_id).unwrap(); + let rows = client.query(sql, &[]).await.unwrap(); + + if rows.is_empty() { + return Ok((Vec::new(), Vec::new(), 0.0f32)); + } + + let columns = rows + .first() + .unwrap() + .columns() + .iter() + .map(|c| c.name().to_string()) + .collect::>(); + let rows = rows + .iter() + .map(|row| { + let mut row_values = Vec::new(); + for i in 0..row.len() { + let value = reflective_get(row, i); + row_values.push(value); + } + row_values + }) + .collect::>>(); + let elasped = start.elapsed().as_millis() as f32; + Ok((columns, rows, elasped)) +} + +#[tauri::command(rename_all = "snake_case")] +pub async fn pgsql_load_relations( + project_name: &str, + schema: &str, + app_state: State<'_, AppState>, +) -> Result> { + // let clients = app_state.client.lock().await; + // let client = clients.as_ref().unwrap().get(project_name).unwrap(); + // let rows = client + // .query( + // r#"--sql SELECT + // tc.constraint_name, + // tc.table_name, + // kcu.column_name, + // ccu.table_name AS foreign_table_name, + // ccu.column_name AS foreign_column_name + // FROM information_schema.table_constraints AS tc + // JOIN information_schema.key_column_usage AS kcu + // ON tc.constraint_name = kcu.constraint_name + // JOIN information_schema.constraint_column_usage AS ccu + // ON ccu.constraint_name = tc.constraint_name + // WHERE constraint_type = 'FOREIGN KEY' + // AND tc.table_schema = $1; + // "#, + // &[&schema], + // ) + // .await + // .unwrap(); + + // let relations = rows + // .iter() + // .map(|row| { + // let constraint_name = row.get(0); + // let table_name = row.get(1); + // let column_name = row.get(2); + // let foreign_table_name = row.get(3); + // let foreign_column_name = row.get(4); + // PostgresqlRelation { + // constraint_name, + // table_name, + // column_name, + // foreign_table_name, + // foreign_column_name, + // } + // }) + // .collect::>(); + + // Ok(relations) + todo!() +} + diff --git a/src-tauri/src/drivers/postgresql.rs b/src-tauri/src/drivers/postgresql.rs deleted file mode 100644 index bb20020..0000000 --- a/src-tauri/src/drivers/postgresql.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::time::Instant; - -use common::projects::postgresql::PostgresqlRelation; -use tauri::{AppHandle, Manager, Result, State}; -use tokio_postgres::{connect, NoTls}; - -use crate::{utils::reflective_get, AppState}; - -#[tauri::command(rename_all = "snake_case")] -pub async fn postgresql_connector( - project_name: &str, - key: &str, - app: AppHandle, -) -> Result> { - let app_state = app.state::(); - let (client, connection) = connect(key, NoTls).await.expect("connection error"); - tokio::spawn(async move { - if let Err(e) = connection.await { - eprintln!("connection error: {}", e); - } - }); - - let schemas = client - .query( - r#" - SELECT schema_name - FROM information_schema.schemata - WHERE schema_name NOT IN ('pg_catalog', 'information_schema'); - "#, - &[], - ) - .await - .unwrap(); - let schemas = schemas.iter().map(|r| r.get(0)).collect(); - let mut clients = app_state.client.lock().await; - let clients = clients.as_mut().unwrap(); - clients.insert(project_name.to_string(), client); - Ok(schemas) -} - -#[tauri::command(rename_all = "snake_case")] -pub async fn select_schema_tables( - project_name: &str, - schema: &str, - app_state: State<'_, AppState>, -) -> Result> { - let clients = app_state.client.lock().await; - let client = clients.as_ref().unwrap().get(project_name).unwrap(); - let tables = client - .query( - r#"--sql - SELECT - table_name, - pg_size_pretty(pg_total_relation_size('"' || table_schema || '"."' || table_name || '"')) AS size - FROM - information_schema.tables - WHERE - table_schema = $1 - ORDER BY - table_name; - "#, - &[&schema], - ) - .await - .unwrap(); - let tables = tables - .iter() - .map(|r| (r.get(0), r.get(1))) - .collect::>(); - Ok(tables) -} - -#[tauri::command(rename_all = "snake_case")] -pub async fn select_sql_result( - project_name: &str, - sql: String, - app_state: State<'_, AppState>, -) -> Result<(Vec, Vec>, f32)> { - let start = Instant::now(); - let clients = app_state.client.lock().await; - let client = clients.as_ref().unwrap().get(project_name).unwrap(); - let rows = client.query(sql.as_str(), &[]).await.unwrap(); - - if rows.is_empty() { - return Ok((Vec::new(), Vec::new(), 0.0f32)); - } - - let columns = rows - .first() - .unwrap() - .columns() - .iter() - .map(|c| c.name().to_string()) - .collect::>(); - let rows = rows - .iter() - .map(|row| { - let mut row_values = Vec::new(); - for i in 0..row.len() { - let value = reflective_get(row, i); - row_values.push(value); - } - row_values - }) - .collect::>>(); - let elasped = start.elapsed().as_millis() as f32; - Ok((columns, rows, elasped)) -} - -#[tauri::command(rename_all = "snake_case")] -pub async fn select_schema_relations( - project_name: &str, - schema: &str, - app_state: State<'_, AppState>, -) -> Result> { - let clients = app_state.client.lock().await; - let client = clients.as_ref().unwrap().get(project_name).unwrap(); - let rows = client - .query( - r#"--sql SELECT - tc.constraint_name, - tc.table_name, - kcu.column_name, - ccu.table_name AS foreign_table_name, - ccu.column_name AS foreign_column_name - FROM information_schema.table_constraints AS tc - JOIN information_schema.key_column_usage AS kcu - ON tc.constraint_name = kcu.constraint_name - JOIN information_schema.constraint_column_usage AS ccu - ON ccu.constraint_name = tc.constraint_name - WHERE constraint_type = 'FOREIGN KEY' - AND tc.table_schema = $1; - "#, - &[&schema], - ) - .await - .unwrap(); - - let relations = rows - .iter() - .map(|row| { - let constraint_name = row.get(0); - let table_name = row.get(1); - let column_name = row.get(2); - let foreign_table_name = row.get(3); - let foreign_column_name = row.get(4); - PostgresqlRelation { - constraint_name, - table_name, - column_name, - foreign_table_name, - foreign_column_name, - } - }) - .collect::>(); - - Ok(relations) -} - diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5b7796c..795e666 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -13,6 +13,7 @@ use std::{collections::BTreeMap, sync::Arc}; use tauri::Manager; use tokio::sync::Mutex; use tokio_postgres::Client; +use tracing::Level; use utils::create_or_open_local_db; pub struct AppState { @@ -32,6 +33,13 @@ impl Default for AppState { } fn main() { + tracing_subscriber::fmt() + .with_file(true) + .with_line_number(true) + .with_level(true) + .with_max_level(Level::INFO) + .init(); + tauri::Builder::default() .manage(AppState::default()) .setup(|app| { @@ -57,16 +65,17 @@ fn main() { Ok(()) }) .invoke_handler(tauri::generate_handler![ - dbs::project::delete_project, - dbs::project::insert_project, - dbs::project::select_projects, - dbs::query::delete_query, - dbs::query::insert_query, - dbs::query::select_queries, - drivers::postgresql::postgresql_connector, - drivers::postgresql::select_schema_relations, - drivers::postgresql::select_schema_tables, - drivers::postgresql::select_sql_result, + dbs::project::project_db_select, + dbs::project::project_db_insert, + dbs::project::project_db_delete, + dbs::query::query_db_select, + dbs::query::query_db_insert, + dbs::query::query_db_delete, + drivers::pgsql::pgsql_connector, + drivers::pgsql::pgsql_load_relations, + drivers::pgsql::pgsql_load_schemas, + drivers::pgsql::pgsql_load_tables, + drivers::pgsql::pgsql_run_query, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 6600032..1042013 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -60,3 +60,4 @@ pub fn reflective_get(row: &Row, index: usize) -> String { }; value.unwrap_or("null".to_string()) } + diff --git a/src/app.rs b/src/app.rs index e69511d..51d2ee3 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,18 +1,19 @@ +use std::collections::VecDeque; + use leptos::*; -use leptos_icons::*; -use thaw::{Button, ButtonSize, Tab, TabLabel, Tabs}; +use leptos_toaster::{Toaster, ToasterPosition}; use crate::{ + dashboard::index::Dashboard, enums::QueryTableLayout, footer::Footer, - query_editor::QueryEditor, - query_table::QueryTable, + performane::Performance, sidebar::index::Sidebar, store::{ - active_project::ActiveProjectStore, + atoms::{QueryPerformanceAtom, QueryPerformanceContext, RunQueryAtom, RunQueryContext}, projects::ProjectsStore, - query::QueryStore, - tabs::{self, TabsStore}, + queries::QueriesStore, + tabs::TabsStore, }, }; @@ -21,69 +22,30 @@ use crate::{ #[component] pub fn App() -> impl IntoView { - provide_context(QueryStore::default()); provide_context(ProjectsStore::default()); - provide_context(create_rw_signal(QueryTableLayout::Grid)); - provide_context(create_rw_signal(0.0f32)); - provide_context(ActiveProjectStore::default()); + provide_context(QueriesStore::default()); + provide_context(RwSignal::new(QueryTableLayout::Grid)); + provide_context::( + RwSignal::new(VecDeque::::new()), + ); + provide_context::(RwSignal::new(RunQueryAtom::default())); provide_context(TabsStore::default()); - let mut tabs = use_context::().unwrap(); view! { -
- -
-
- - - -
- {format!("Tab {}", index + 1)} - -
-
- - - - } - } - /> - -
- -
-
+ +
+ +
+
+ +
+
+
+
+ +
-
+ } } diff --git a/src/context_menu/mod.rs b/src/context_menu/mod.rs deleted file mode 100644 index d963c78..0000000 --- a/src/context_menu/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod siderbar; diff --git a/src/context_menu/siderbar.rs b/src/context_menu/siderbar.rs deleted file mode 100644 index 786fc14..0000000 --- a/src/context_menu/siderbar.rs +++ /dev/null @@ -1,19 +0,0 @@ -use leptos::ev::MouseEvent; - -use crate::invoke::{InvokeContextMenuArgs, InvokeContextMenuItem, InvokeContextMenuPosition}; - -pub fn context_menu<'a>(event: &MouseEvent) -> InvokeContextMenuArgs<'a> { - InvokeContextMenuArgs { - pos: Some(InvokeContextMenuPosition { - x: event.x() as f64, - y: event.y() as f64, - is_absolute: Some(true), - }), - items: Some(vec![InvokeContextMenuItem { - label: Some("test"), - event: Some("my_first_item"), - payload: Some("test2"), - ..Default::default() - }]), - } -} diff --git a/src/dashboard/index.rs b/src/dashboard/index.rs new file mode 100644 index 0000000..33ebd0d --- /dev/null +++ b/src/dashboard/index.rs @@ -0,0 +1,46 @@ +use leptos::{logging::log, *}; +use leptos_icons::Icon; +use thaw::{Tab, TabLabel, Tabs}; + +use crate::store::tabs; + +use super::{query_editor::QueryEditor, query_table::QueryTable}; + +#[component] +pub fn Dashboard() -> impl IntoView { + let tabs_store = expect_context::(); + create_effect(move |_| { + log!("Selected tab: {}", tabs_store.selected_tab.get()); + }); + + view! { + + + +
+ {format!("Tab {}", index + 1)} + +
+
+ + + + } + } + /> + +
+ } +} + diff --git a/src/dashboard/mod.rs b/src/dashboard/mod.rs new file mode 100644 index 0000000..6065177 --- /dev/null +++ b/src/dashboard/mod.rs @@ -0,0 +1,4 @@ +pub mod index; +pub mod query_editor; +pub mod query_table; + diff --git a/src/dashboard/query_editor.rs b/src/dashboard/query_editor.rs new file mode 100644 index 0000000..59edf6a --- /dev/null +++ b/src/dashboard/query_editor.rs @@ -0,0 +1,128 @@ +use std::{cell::RefCell, rc::Rc, sync::Arc}; + +use futures::lock::Mutex; +use leptos::*; +use leptos_use::{use_document, use_event_listener}; +use monaco::{ + api::{CodeEditor, CodeEditorOptions, TextModel}, + sys::{ + editor::{IDimension, IEditorMinimapOptions}, + KeyCode, KeyMod, + }, +}; +use wasm_bindgen::{closure::Closure, JsCast}; + +use crate::{ + modals::add_custom_query::AddCustomQuery, + store::{projects::ProjectsStore, tabs::TabsStore}, +}; + +pub type ModelCell = Rc>>; +pub const MODE_ID: &str = "pgsql"; + +#[component] +pub fn QueryEditor(index: usize) -> impl IntoView { + let tabs_store = expect_context::(); + let active_project = move || match tabs_store.selected_projects.get().get(index) { + Some(project) => Some(project.clone()), + _ => None, + }; + let projects_store = expect_context::(); + let project_driver = projects_store.select_driver_by_project(active_project().as_deref()); + let tabs_store_rc = Rc::new(RefCell::new(tabs_store)); + let show = create_rw_signal(false); + let _ = use_event_listener(use_document(), ev::keydown, move |event| { + if event.key() == "Escape" { + show.set(false); + } + }); + let node_ref = create_node_ref(); + + { + let tabs_store = tabs_store_rc.clone(); + node_ref.on_load(move |node| { + let div_element: &web_sys::HtmlDivElement = &node; + let html_element = div_element.unchecked_ref::(); + let options = CodeEditorOptions::default().to_sys_options(); + let text_model = + TextModel::create("# Add your SQL query here...", Some(MODE_ID), None).unwrap(); + options.set_model(Some(text_model.as_ref())); + options.set_language(Some(MODE_ID)); + options.set_automatic_layout(Some(true)); + options.set_dimension(Some(&IDimension::new(0, 240))); + let minimap_settings = IEditorMinimapOptions::default(); + minimap_settings.set_enabled(Some(false)); + options.set_minimap(Some(&minimap_settings)); + + let e = CodeEditor::create(html_element, Some(options)); + let keycode = KeyMod::win_ctrl() as u32 | KeyCode::Enter.to_value(); + // TODO: Fix this + e.as_ref().add_command( + keycode.into(), + Closure::::new(|| ()).as_ref().unchecked_ref(), + None, + ); + + // TODO: Fix this + let e = Rc::new(RefCell::new(Some(e))); + tabs_store.borrow_mut().add_editor(e); + }); + }; + + let tabs_store_arc = Arc::new(Mutex::new(tabs_store)); + let run_query = create_action(move |tabs_store: &Arc>| { + let tabs_store = tabs_store.clone(); + async move { + tabs_store.lock().await.run_query().await; + } + }); + + let _ = use_event_listener(node_ref, ev::keydown, { + let tabs_store = tabs_store_arc.clone(); + + move |event| { + if event.key() == "Enter" && event.ctrl_key() { + run_query.dispatch(tabs_store.clone()); + } + } + }); + + view! { +
+
+
}> + +
+ {active_project} +
+ +
}> + +
+ + +
+ +
+ + } +} + diff --git a/src/dashboard/query_table.rs b/src/dashboard/query_table.rs new file mode 100644 index 0000000..ae97342 --- /dev/null +++ b/src/dashboard/query_table.rs @@ -0,0 +1,59 @@ +use leptos::*; +use leptos_icons::*; + +use crate::{ + enums::QueryTableLayout, + grid_view::GridView, + record_view::RecordView, + store::{atoms::RunQueryContext, tabs::TabsStore}, +}; + +#[component] +pub fn QueryTable() -> impl IntoView { + let tabs_store = expect_context::(); + let table_view = expect_context::>(); + let is_query_running = expect_context::(); + + view! { + + +

"Running query..."

+ + } + } + > + + {move || match tabs_store.select_active_editor_sql_result() { + None => { + view! { +
+ "No data to display" +
+ } + } + Some(_) => { + view! { +
+ {match table_view.get() { + QueryTableLayout::Grid => view! { }, + QueryTableLayout::Records => view! { }, + }} + +
+ } + } + }} + +
+ } +} + diff --git a/src/databases/mod.rs b/src/databases/mod.rs new file mode 100644 index 0000000..173f9d2 --- /dev/null +++ b/src/databases/mod.rs @@ -0,0 +1,2 @@ +pub mod pgsql; + diff --git a/src/databases/pgsql/driver.rs b/src/databases/pgsql/driver.rs new file mode 100644 index 0000000..624806e --- /dev/null +++ b/src/databases/pgsql/driver.rs @@ -0,0 +1,172 @@ +use ahash::AHashMap; +use common::{ + enums::ProjectConnectionStatus, + types::pgsql::{PgsqlLoadSchemas, PgsqlLoadTables, PgsqlRunQuery}, +}; +use leptos::{error::Result, expect_context, RwSignal, SignalGet, SignalSet, SignalUpdate}; +use rsql::set_running_query; +use tauri_sys::tauri::invoke; + +use crate::{ + invoke::{ + Invoke, InvokePgsqlConnectorArgs, InvokePgsqlLoadSchemasArgs, InvokePgsqlLoadTablesArgs, + InvokePgsqlRunQueryArgs, + }, + store::{ + atoms::{QueryPerformanceAtom, QueryPerformanceContext, RunQueryAtom, RunQueryContext}, + tabs::TabsStore, + }, +}; + +#[derive(Debug, Clone, Copy)] +pub struct Pgsql<'a> { + pub project_id: RwSignal, + user: Option<&'a str>, + password: Option<&'a str>, + host: Option<&'a str>, + port: Option<&'a str>, + pub status: RwSignal, + pub schemas: RwSignal>, + pub tables: RwSignal>>, +} + +impl<'a> Pgsql<'a> { + pub fn new(project_id: String) -> Self { + Self { + project_id: RwSignal::new(project_id), + status: RwSignal::default(), + schemas: RwSignal::default(), + tables: RwSignal::default(), + user: None, + password: None, + host: None, + port: None, + } + } + + pub async fn connector(&self) -> Result { + self + .status + .update(|prev| *prev = ProjectConnectionStatus::Connecting); + let connection_string = self.generate_connection_string(); + let status = invoke::<_, ProjectConnectionStatus>( + Invoke::PgsqlConnector.as_ref(), + &InvokePgsqlConnectorArgs { + project_id: &self.project_id.get(), + key: Some(&connection_string), + }, + ) + .await + .unwrap(); + if status == ProjectConnectionStatus::Connected { + self.load_schemas().await; + } + self.status.update(|prev| *prev = status.clone()); + Ok(status) + } + + pub async fn load_schemas(&self) { + let schemas = invoke::<_, PgsqlLoadSchemas>( + Invoke::PgsqlLoadSchemas.as_ref(), + &InvokePgsqlLoadSchemasArgs { + project_id: &self.project_id.get(), + }, + ) + .await + .unwrap(); + self.schemas.set(schemas); + } + + pub async fn load_tables(&self, schema: &str) { + if self.tables.get().contains_key(schema) { + return; + } + let tables = invoke::<_, PgsqlLoadTables>( + Invoke::PgsqlLoadTables.as_ref(), + &InvokePgsqlLoadTablesArgs { + project_id: &self.project_id.get(), + schema, + }, + ) + .await + .unwrap(); + self.tables.update(|prev| { + prev.insert(schema.to_owned(), tables); + }); + } + + #[set_running_query] + pub async fn run_default_table_query(&self, sql: &str) { + let tabs_store = expect_context::(); + + let selected_projects = tabs_store.selected_projects.get(); + let project_id = selected_projects.get(tabs_store.convert_selected_tab_to_index()); + + if !selected_projects.is_empty() + && project_id.is_some_and(|id| id.as_str() != &self.project_id.get()) + { + tabs_store.add_tab(&self.project_id.get()); + } + + tabs_store.set_editor_value(sql); + tabs_store.selected_projects.update(|prev| { + let index = tabs_store.convert_selected_tab_to_index(); + match prev.get_mut(index) { + Some(project) => *project = self.project_id.get().clone(), + None => prev.push(self.project_id.get().clone()), + } + }); + + let query = invoke::<_, PgsqlRunQuery>( + Invoke::PgsqlRunQuery.as_ref(), + &InvokePgsqlRunQueryArgs { + project_id: &self.project_id.get(), + sql, + }, + ) + .await + .unwrap(); + let (cols, rows, query_time) = query; + tabs_store.sql_results.update(|prev| { + let index = tabs_store.convert_selected_tab_to_index(); + match prev.get_mut(index) { + Some(sql_result) => *sql_result = (cols, rows), + None => prev.push((cols, rows)), + } + }); + let qp_store = expect_context::(); + qp_store.update(|prev| { + let new = QueryPerformanceAtom::new(prev.len(), sql, query_time); + prev.push_front(new); + }); + } + + pub fn select_tables_by_schema(&self, schema: &str) -> Option> { + self.tables.get().get(schema).cloned() + } + + pub fn load_connection_details( + &mut self, + user: &'a str, + password: &'a str, + host: &'a str, + port: &'a str, + ) { + self.user = Some(user); + self.password = Some(password); + self.host = Some(host); + self.port = Some(port); + } + + fn generate_connection_string(&self) -> String { + let connection_string = format!( + "user={} password={} host={} port={}", + self.user.as_ref().unwrap(), + self.password.as_ref().unwrap(), + self.host.as_ref().unwrap(), + self.port.as_ref().unwrap(), + ); + connection_string + } +} + diff --git a/src/databases/pgsql/index.rs b/src/databases/pgsql/index.rs new file mode 100644 index 0000000..d466fba --- /dev/null +++ b/src/databases/pgsql/index.rs @@ -0,0 +1,165 @@ +use std::rc::Rc; + +use leptos::*; +use leptos_icons::*; +use leptos_toaster::{Toast, ToastId, ToastVariant, Toasts}; + +use super::{driver::Pgsql, schema::Schema}; +use crate::store::{projects::ProjectsStore, tabs::TabsStore}; +use common::enums::ProjectConnectionStatus; + +#[component] +pub fn Pgsql(project_id: String) -> impl IntoView { + let project_id = Rc::new(project_id); + let tabs_store = expect_context::(); + let projects_store = expect_context::(); + let project_details = projects_store.select_project_by_name(&project_id).unwrap(); + let connection_params = project_details + .split(':') + .map(String::from) + .collect::>(); + let connection_params = connection_params + .into_iter() + .skip(1) + .map(|s| { + let kv = s.split('=').collect::>(); + kv[1].to_owned() + }) + .collect::>(); + let connection_params = Box::leak(connection_params.into_boxed_slice()); + // [user, password, host, port] + let mut pgsql = Pgsql::new(project_id.clone().to_string()); + { + pgsql.load_connection_details( + &connection_params[0], + &connection_params[1], + &connection_params[2], + &connection_params[3], + ); + } + let toast_context = expect_context::(); + let create_toast = move |variant: ToastVariant, title: String| { + let toast_id = ToastId::new(); + toast_context.toast( + view! { }, + Some(toast_id), + None, // options + ); + }; + + let connect = create_action(move |pgsql: &Pgsql| { + let pgsql = *pgsql; + async move { + let status = pgsql.connector().await.unwrap(); + match status { + ProjectConnectionStatus::Connected => { + create_toast(ToastVariant::Success, "Connected to project".into()); + } + ProjectConnectionStatus::Failed => { + create_toast(ToastVariant::Error, "Failed to connect to project".into()) + } + _ => create_toast(ToastVariant::Warning, "Failed to connect to project".into()), + } + } + }); + let delete_project = create_action( + move |(projects_store, project_id): &(ProjectsStore, String)| { + let projects_store = *projects_store; + let project_id = project_id.clone(); + async move { + projects_store.delete_project(&project_id).await; + } + }, + ); + let connect = move || { + if pgsql.status.get() == ProjectConnectionStatus::Connected { + return; + } + connect.dispatch(pgsql); + }; + + view! { + +
+
+ +
+ + +
+
+
+ + } + } + /> + + +
+
+
+ } +} + diff --git a/src/databases/pgsql/mod.rs b/src/databases/pgsql/mod.rs new file mode 100644 index 0000000..8487ba9 --- /dev/null +++ b/src/databases/pgsql/mod.rs @@ -0,0 +1,5 @@ +pub mod driver; +pub mod index; +pub mod schema; +pub mod table; + diff --git a/src/databases/pgsql/schema.rs b/src/databases/pgsql/schema.rs new file mode 100644 index 0000000..75c5ccd --- /dev/null +++ b/src/databases/pgsql/schema.rs @@ -0,0 +1,73 @@ +use std::rc::Rc; + +use leptos::*; +use leptos_icons::*; + +use super::{driver::Pgsql, table::Table}; + +#[component] +pub fn Schema(schema: String) -> impl IntoView { + let (show, set_show) = create_signal(false); + let (is_loading, set_is_loading) = create_signal(false); + let schema = Rc::new(schema); + let pgsql = expect_context::(); + let load_tables = create_action(move |schema: &String| { + let schema = schema.clone(); + async move { + pgsql.load_tables(&schema).await; + set_is_loading(false); + set_show(!show()); + } + }); + + view! { +
+ +
+ + } + } + } + /> + + +
+
+ } +} + diff --git a/src/databases/pgsql/table.rs b/src/databases/pgsql/table.rs new file mode 100644 index 0000000..81d661b --- /dev/null +++ b/src/databases/pgsql/table.rs @@ -0,0 +1,42 @@ +use std::rc::Rc; + +use leptos::*; +use leptos_icons::*; + +use crate::databases::pgsql::driver::Pgsql; + +#[component] +pub fn Table(table: (String, String), schema: String) -> impl IntoView { + let table = Rc::new(table); + let schema = Rc::new(schema); + let pgsql = expect_context::(); + let query = create_action(move |(schema, table, pgsql): &(String, String, Pgsql)| { + let pgsql = *pgsql; + let schema = schema.clone(); + let table = table.clone(); + + async move { + pgsql + .run_default_table_query(&format!("SELECT * FROM {}.{} LIMIT 100;", schema, table)) + .await; + } + }); + + view! { +
+ +
+ +

{table.0.to_string()}

+
+

{table.1.to_string()}

+
+ } +} + diff --git a/src/footer.rs b/src/footer.rs index 8c6b965..cf56bf5 100644 --- a/src/footer.rs +++ b/src/footer.rs @@ -1,43 +1,29 @@ use leptos::*; use leptos_icons::*; -use crate::{enums::QueryTableLayout, store::active_project::ActiveProjectStore}; +use crate::enums::QueryTableLayout; #[component] pub fn Footer() -> impl IntoView { - let table_view = use_context::>().unwrap(); - let acitve_project = use_context::().unwrap(); - let sql_timer = use_context::>().unwrap(); - let formatted_timer = create_memo(move |_| format!("Query complete: {}ms", sql_timer.get())); + let table_view = expect_context::>(); view! { -
-
-
}> -
-

Selected project:

-

{move || acitve_project.0.get()}

-
- - -
-

{formatted_timer}

- - -
+ + +
} } diff --git a/src/grid_view.rs b/src/grid_view.rs index f44cac4..450a6b6 100644 --- a/src/grid_view.rs +++ b/src/grid_view.rs @@ -4,7 +4,7 @@ use crate::store::tabs::TabsStore; #[component] pub fn GridView() -> impl IntoView { - let tabs_store = use_context::().unwrap(); + let tabs_store = expect_context::(); view! { diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs deleted file mode 100644 index f83fd17..0000000 --- a/src/hooks/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod use_context_menu; diff --git a/src/hooks/use_context_menu.rs b/src/hooks/use_context_menu.rs deleted file mode 100644 index 2f73d47..0000000 --- a/src/hooks/use_context_menu.rs +++ /dev/null @@ -1,31 +0,0 @@ -use futures::StreamExt; -use leptos::{html::*, *}; -use leptos_use::use_event_listener; -use tauri_sys::{event::listen, tauri::invoke}; -use web_sys::MouseEvent; - -use crate::invoke::{Invoke, InvokeContextMenuArgs}; - -pub fn use_context_menu(f: F) -> NodeRef
-where - F: Fn(&MouseEvent) -> InvokeContextMenuArgs<'static> + 'static + Clone, -{ - let node_ref = create_node_ref(); - let _ = use_event_listener(node_ref, ev::contextmenu, move |event| { - let f = f.clone(); - spawn_local(async move { - invoke::<_, ()>(&Invoke::plugin_context_menu.to_string(), &f(&event)) - .await - .unwrap(); - }); - }); - - spawn_local(async move { - let mut evt = listen::("my_first_item").await.expect("error"); - while let Some(event) = evt.next().await { - logging::log!("{:?}", event.payload); - } - }); - - node_ref -} diff --git a/src/invoke.rs b/src/invoke.rs index 215ac47..ed80513 100644 --- a/src/invoke.rs +++ b/src/invoke.rs @@ -1,138 +1,106 @@ use std::fmt::Display; -use common::enums::Project; use serde::{Deserialize, Serialize}; -#[allow(non_camel_case_types)] pub enum Invoke { - delete_project, - delete_query, - insert_project, - insert_query, - plugin_context_menu, - postgresql_connector, - select_projects, - select_queries, - select_schema_relations, - select_schema_tables, - select_sql_result, + ProjectDbSelect, + ProjectDbInsert, + ProjectDbDelete, + + QueryDbSelect, + QueryDbInsert, + QueryDbDelete, + + PgsqlConnector, + PgsqlLoadSchemas, + PgsqlLoadTables, + #[allow(dead_code)] + PgsqlLoadRelations, + PgsqlRunQuery, } impl Display for Invoke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Invoke::delete_project => write!(f, "delete_project"), - Invoke::delete_query => write!(f, "delete_query"), - Invoke::insert_project => write!(f, "insert_project"), - Invoke::insert_query => write!(f, "insert_query"), - Invoke::postgresql_connector => write!(f, "postgresql_connector"), - Invoke::plugin_context_menu => write!(f, "plugin:context_menu|show_context_menu"), - Invoke::select_projects => write!(f, "select_projects"), - Invoke::select_queries => write!(f, "select_queries"), - Invoke::select_schema_relations => write!(f, "select_schema_relations"), - Invoke::select_schema_tables => write!(f, "select_schema_tables"), - Invoke::select_sql_result => write!(f, "select_sql_result"), + Invoke::ProjectDbSelect => write!(f, "project_db_select"), + Invoke::ProjectDbInsert => write!(f, "project_db_insert"), + Invoke::ProjectDbDelete => write!(f, "project_db_delete"), + + Invoke::QueryDbSelect => write!(f, "query_db_select"), + Invoke::QueryDbInsert => write!(f, "query_db_insert"), + Invoke::QueryDbDelete => write!(f, "query_db_delete"), + + Invoke::PgsqlConnector => write!(f, "pgsql_connector"), + Invoke::PgsqlLoadRelations => write!(f, "pgsql_load_relations"), + Invoke::PgsqlLoadTables => write!(f, "pgsql_load_tables"), + Invoke::PgsqlLoadSchemas => write!(f, "pgsql_load_schemas"), + Invoke::PgsqlRunQuery => write!(f, "pgsql_run_query"), + } + } +} + +impl AsRef for Invoke { + fn as_ref(&self) -> &str { + match *self { + Invoke::ProjectDbSelect => "project_db_select", + Invoke::ProjectDbInsert => "project_db_insert", + Invoke::ProjectDbDelete => "project_db_delete", + + Invoke::QueryDbSelect => "query_db_select", + Invoke::QueryDbInsert => "query_db_insert", + Invoke::QueryDbDelete => "query_db_delete", + + Invoke::PgsqlConnector => "pgsql_connector", + Invoke::PgsqlLoadSchemas => "pgsql_load_schemas", + Invoke::PgsqlLoadTables => "pgsql_load_tables", + Invoke::PgsqlLoadRelations => "pgsql_load_relations", + Invoke::PgsqlRunQuery => "pgsql_run_query", } } } #[derive(Serialize, Deserialize)] -pub struct InvokePostgresConnectionArgs<'a> { - pub project_name: &'a str, - pub key: &'a str, +pub struct InvokePgsqlConnectorArgs<'a> { + pub project_id: &'a str, + pub key: Option<&'a str>, } #[derive(Serialize, Deserialize)] -pub struct InvokeSchemaRelationsArgs<'a> { - pub project_name: &'a str, - pub schema: &'a str, +pub struct InvokePgsqlLoadSchemasArgs<'a> { + pub project_id: &'a str, } #[derive(Serialize, Deserialize)] -pub struct InvokeSchemaTablesArgs<'a> { - pub project_name: &'a str, +pub struct InvokePgsqlLoadTablesArgs<'a> { + pub project_id: &'a str, pub schema: &'a str, } #[derive(Serialize, Deserialize)] -pub struct InvokeSqlResultArgs<'a> { - pub project_name: &'a str, +pub struct InvokePgsqlRunQueryArgs<'a> { + pub project_id: &'a str, pub sql: &'a str, } #[derive(Serialize, Deserialize)] -pub struct InvokeSelectProjectsArgs; - -#[derive(Serialize, Deserialize)] -pub struct InvokeInsertProjectArgs { - pub project: Project, +pub struct InvokeProjectDbInsertArgs<'a> { + pub project_id: &'a str, + pub project_details: &'a str, } #[derive(Serialize, Deserialize)] -pub struct InvokeDeleteProjectArgs<'a> { - pub project_name: &'a str, +pub struct InvokeProjectDbDeleteArgs<'a> { + pub project_id: &'a str, } #[derive(Serialize, Deserialize)] -pub struct InvokeInsertQueryArgs<'a> { - pub key: &'a str, +pub struct InvokeQueryDbInsertArgs<'a> { + pub query_id: &'a str, pub sql: &'a str, } #[derive(Serialize, Deserialize)] -pub struct InvokeSelectQueriesArgs; - -#[derive(Serialize, Deserialize)] -pub struct InvokeDeleteQueryArgs<'a> { - pub key: &'a str, +pub struct InvokeQueryDbDeleteArgs<'a> { + pub query_id: &'a str, } -#[derive(Default, Serialize, Deserialize)] -pub struct InvokeContextMenuArgs<'a> { - pub pos: Option, - #[serde(borrow)] - pub items: Option>>, -} - -#[derive(Serialize, Deserialize)] -pub struct InvokeContextMenuItem<'a> { - pub label: Option<&'a str>, - pub disabled: Option, - pub shortcut: Option<&'a str>, - pub event: Option<&'a str>, - pub payload: Option<&'a str>, - pub subitems: Option>>, - pub icon: Option>, - pub checked: Option, - pub is_separator: Option, -} - -impl<'a> Default for InvokeContextMenuItem<'a> { - fn default() -> Self { - Self { - label: None, - disabled: Some(false), - shortcut: None, - event: None, - payload: None, - subitems: None, - icon: None, - checked: Some(false), - is_separator: Some(false), - } - } -} - -#[derive(Serialize, Deserialize)] -pub struct InvokeContextItemIcon<'a> { - pub path: &'a str, - pub width: Option, - pub height: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct InvokeContextMenuPosition { - pub x: f64, - pub y: f64, - pub is_absolute: Option, -} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fce1c62 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,32 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, ItemFn}; + +#[proc_macro_attribute] +pub fn set_running_query(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + + let vis = &input.vis; + let sig = &input.sig; + let block = &input.block; + + let gen = quote! { + #vis #sig { + let run_query_atom = expect_context::(); + run_query_atom.set(RunQueryAtom { is_running: true }); + + let result = async { + #block + }.await; + + run_query_atom.set(RunQueryAtom { is_running: false }); + + result + } + }; + + TokenStream::from(gen) +} + diff --git a/src/main.rs b/src/main.rs index d6bdcb0..e9198d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,14 @@ +#![feature(pattern)] + mod app; -mod context_menu; +mod dashboard; +mod databases; mod enums; mod footer; mod grid_view; -mod hooks; mod invoke; mod modals; -mod query_editor; -mod query_table; +mod performane; mod record_view; mod sidebar; mod store; diff --git a/src/modals/add_custom_query.rs b/src/modals/add_custom_query.rs new file mode 100644 index 0000000..9f9c52e --- /dev/null +++ b/src/modals/add_custom_query.rs @@ -0,0 +1,70 @@ +use std::rc::Rc; + +use common::enums::Drivers; +use leptos::*; +use thaw::{Modal, ModalFooter}; + +use crate::store::queries::QueriesStore; + +#[component] +pub fn AddCustomQuery(show: RwSignal, project_id: String, driver: Drivers) -> impl IntoView { + let project_id = Rc::new(project_id); + let project_id_clone = project_id.clone(); + let query_store = expect_context::(); + let (title, set_title) = create_signal(String::new()); + let insert_query = create_action( + move |(query_db, project_id, title, driver): &(QueriesStore, String, String, Drivers)| { + let query_db_clone = *query_db; + let project_id = project_id.clone(); + let title = title.clone(); + let driver = driver.clone(); + async move { + query_db_clone + .insert_query(&project_id, &title, &driver) + .await; + } + }, + ); + + view! { + +
+

Project: {&*project_id_clone}

+ +
+ + +
+ + +
+
+
+ } +} + diff --git a/src/modals/add_pgsql_connection.rs b/src/modals/add_pgsql_connection.rs new file mode 100644 index 0000000..9460a16 --- /dev/null +++ b/src/modals/add_pgsql_connection.rs @@ -0,0 +1,131 @@ +use common::enums::Drivers; +use leptos::*; +use thaw::{Modal, ModalFooter}; + +use crate::store::projects::ProjectsStore; + +#[derive(Default, Clone)] +struct ConnectionDetails { + pub project_id: String, + pub driver: Drivers, + pub user: String, + pub password: String, + pub host: String, + pub port: String, +} + +impl IntoIterator for ConnectionDetails { + type Item = String; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + vec![ + self.project_id.to_owned(), + self.user.to_owned(), + self.password.to_owned(), + self.host.to_owned(), + self.port.to_owned(), + ] + .into_iter() + } +} + +#[component] +pub fn AddPgsqlConnection(show: RwSignal) -> impl IntoView { + let projects_store = expect_context::(); + let params = create_rw_signal(ConnectionDetails { + driver: Drivers::PGSQL, + ..Default::default() + }); + let save_project = create_action(move |(project_id, project_details): &(String, String)| { + let project_id = project_id.clone(); + let project_details = project_details.clone(); + async move { + projects_store + .insert_project(&project_id, &project_details) + .await; + show.set(false); + } + }); + + view! { + +
+ + + + + + + + + +
+ + +
+ + +
+
+
+ } +} + diff --git a/src/modals/connection.rs b/src/modals/connection.rs deleted file mode 100644 index 0e1430a..0000000 --- a/src/modals/connection.rs +++ /dev/null @@ -1,125 +0,0 @@ -use common::{ - drivers::postgresql::Postgresql as PostgresqlDriver, - enums::{Drivers, Project}, - projects::postgresql::Postgresql, -}; -use leptos::*; -use tauri_sys::tauri::invoke; -use thaw::{Modal, ModalFooter}; - -use crate::{ - invoke::{Invoke, InvokeInsertProjectArgs}, - store::projects::ProjectsStore, -}; - -#[component] -pub fn Connection(show: RwSignal) -> impl IntoView { - let projects_store = use_context::().unwrap(); - let (driver, _set_driver) = create_signal(Drivers::POSTGRESQL); - let (project, set_project) = create_signal(String::new()); - let (db_user, set_db_user) = create_signal(String::new()); - let (db_password, set_db_password) = create_signal(String::new()); - let (db_host, set_db_host) = create_signal(String::new()); - let (db_port, set_db_port) = create_signal(String::new()); - let save_project = create_action(move |project_details: &Project| { - let project_details = project_details.clone(); - async move { - let project = invoke::<_, Project>( - &Invoke::insert_project.to_string(), - &InvokeInsertProjectArgs { - project: project_details, - }, - ) - .await - .unwrap(); - projects_store.insert_project(project).unwrap(); - show.set(false); - } - }); - - view! { - -
- - - - - - - - - -
- - -
- - -
-
-
- } -} - diff --git a/src/modals/custom_query.rs b/src/modals/custom_query.rs deleted file mode 100644 index 48e5345..0000000 --- a/src/modals/custom_query.rs +++ /dev/null @@ -1,90 +0,0 @@ -use leptos::*; -use thaw::{Modal, ModalFooter}; - -use crate::store::{ - active_project::ActiveProjectStore, projects::ProjectsStore, query::QueryStore, -}; - -#[component] -pub fn CustomQuery(show: RwSignal) -> impl IntoView { - let projects_store = use_context::().unwrap(); - let query_store = use_context::().unwrap(); - let (query_title, set_query_title) = create_signal(String::new()); - let projects = create_memo(move |_| projects_store.get_projects().unwrap()); - let active_project = use_context::().unwrap(); - let (project_name, set_project_name) = create_signal(active_project.0.get().unwrap_or_default()); - create_effect(move |_| { - if !projects.get().is_empty() { - set_project_name(projects.get()[0].clone()); - } - }); - - let insert_query = create_action( - move |(query_db, key, project_name): &(QueryStore, String, String)| { - let query_db_clone = *query_db; - let key = key.clone(); - let project_name = project_name.clone(); - async move { - query_db_clone - .insert_query(&key, &project_name) - .await - .unwrap(); - } - }, - ); - - view! { - -
- - -
- - -
- - -
-
-
- } -} - diff --git a/src/modals/mod.rs b/src/modals/mod.rs index d34fe1c..b3cd075 100644 --- a/src/modals/mod.rs +++ b/src/modals/mod.rs @@ -1,2 +1,3 @@ -pub mod connection; -pub mod custom_query; +pub mod add_custom_query; +pub mod add_pgsql_connection; + diff --git a/src/performane.rs b/src/performane.rs new file mode 100644 index 0000000..d696904 --- /dev/null +++ b/src/performane.rs @@ -0,0 +1,29 @@ +use leptos::*; + +use crate::store::atoms::QueryPerformanceContext; + +#[component] +pub fn Performance() -> impl IntoView { + let performance = expect_context::(); + + view! { +
+

"Performance"

+
+ + {item.message.clone()} +
+ } + } + /> + +
+
+ } +} + diff --git a/src/query_editor.rs b/src/query_editor.rs deleted file mode 100644 index 79689df..0000000 --- a/src/query_editor.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::{cell::RefCell, rc::Rc, sync::Arc}; - -use futures::lock::Mutex; -use leptos::*; -use leptos_use::{use_document, use_event_listener}; -use monaco::{ - api::{CodeEditor, CodeEditorOptions, TextModel}, - sys::{ - editor::{IDimension, IEditorMinimapOptions}, - KeyCode, KeyMod, - }, -}; -use wasm_bindgen::{closure::Closure, JsCast}; - -use crate::{modals::custom_query::CustomQuery, store::tabs::TabsStore}; - -pub type ModelCell = Rc>>; -pub const MODE_ID: &str = "pgsql"; - -#[component] -pub fn QueryEditor() -> impl IntoView { - let tabs_store = Rc::new(RefCell::new(use_context::().unwrap())); - let show = create_rw_signal(false); - let _ = use_event_listener(use_document(), ev::keydown, move |event| { - if event.key() == "Escape" { - show.set(false); - } - }); - let node_ref = create_node_ref(); - - let tabs_store_clone = tabs_store.clone(); - node_ref.on_load(move |node| { - let div_element: &web_sys::HtmlDivElement = &node; - let html_element = div_element.unchecked_ref::(); - let options = CodeEditorOptions::default().to_sys_options(); - let text_model = - TextModel::create("SELECT * FROM users LIMIT 100;", Some(MODE_ID), None).unwrap(); - options.set_model(Some(text_model.as_ref())); - options.set_language(Some(MODE_ID)); - options.set_automatic_layout(Some(true)); - options.set_dimension(Some(&IDimension::new(0, 240))); - let minimap_settings = IEditorMinimapOptions::default(); - minimap_settings.set_enabled(Some(false)); - options.set_minimap(Some(&minimap_settings)); - - let e = CodeEditor::create(html_element, Some(options)); - let keycode = KeyMod::win_ctrl() as u32 | KeyCode::Enter.to_value(); - // TODO: Fix this - e.as_ref().add_command( - keycode.into(), - Closure::::new(|| ()).as_ref().unchecked_ref(), - None, - ); - - // TODO: Fix this - let e = Rc::new(RefCell::new(Some(e))); - tabs_store_clone.borrow_mut().add_editor(e); - }); - let tabs_store = Arc::new(Mutex::new(use_context::().unwrap())); - let run_query = create_action(move |tabs_store: &Arc>| { - let tabs_store = tabs_store.clone(); - async move { - tabs_store.lock().await.run_query().await.unwrap(); - } - }); - let tabs_store_clone = tabs_store.clone(); - let _ = use_event_listener(node_ref, ev::keydown, move |event| { - if event.key() == "Enter" && event.ctrl_key() { - run_query.dispatch(tabs_store_clone.clone()); - } - }); - - view! { -
- -
-
- - -
-
-
- } -} - diff --git a/src/query_table.rs b/src/query_table.rs deleted file mode 100644 index 417d5e5..0000000 --- a/src/query_table.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{ - enums::QueryTableLayout, grid_view::GridView, record_view::RecordView, store::tabs::TabsStore, -}; -use leptos::*; - -#[component] -pub fn QueryTable() -> impl IntoView { - let tabs_store = use_context::().unwrap(); - let table_view = use_context::>().unwrap(); - - view! { - "Loading..."

}> - {move || match tabs_store.select_active_editor_sql_result() { - None => view! { <>"No data to display" }, - Some(_) => { - view! { - <> - {match table_view.get() { - QueryTableLayout::Grid => view! { }, - QueryTableLayout::Records => view! { }, - }} - - } - } - }} - -
- } -} - diff --git a/src/record_view.rs b/src/record_view.rs index 74122f9..7c7cdde 100644 --- a/src/record_view.rs +++ b/src/record_view.rs @@ -4,7 +4,7 @@ use crate::store::tabs::TabsStore; #[component] pub fn RecordView() -> impl IntoView { - let tabs_store = use_context::().unwrap(); + let tabs_store = expect_context::(); let columns = tabs_store.select_active_editor_sql_result().unwrap().0; let first_row = tabs_store .select_active_editor_sql_result() diff --git a/src/sidebar/index.rs b/src/sidebar/index.rs index 8b84fef..09a0425 100644 --- a/src/sidebar/index.rs +++ b/src/sidebar/index.rs @@ -1,47 +1,36 @@ -use common::enums::Project; use leptos::*; use leptos_use::{use_document, use_event_listener}; -use tauri_sys::tauri::invoke; use crate::{ - context_menu::siderbar::context_menu, - hooks::use_context_menu, - invoke::{Invoke, InvokeSelectProjectsArgs}, - modals::connection::Connection, - store::projects::ProjectsStore, + databases::pgsql::index::Pgsql, + modals::add_pgsql_connection::AddPgsqlConnection, + store::{projects::ProjectsStore, queries::QueriesStore}, }; +use common::enums::Drivers; -use super::{project::Project, queries::Queries}; +use super::queries::Queries; #[component] pub fn Sidebar() -> impl IntoView { - let projects_state = use_context::().unwrap(); - let node_ref = use_context_menu::use_context_menu(context_menu); + let projects_store = expect_context::(); + let queries_store = expect_context::(); let show = create_rw_signal(false); let _ = use_event_listener(use_document(), ev::keydown, move |event| { if event.key() == "Escape" { show.set(false); } }); - create_resource( - move || projects_state.0.get(), + let _ = create_resource( + || {}, move |_| async move { - let projects = invoke::<_, Vec<(String, Project)>>( - &Invoke::select_projects.to_string(), - &InvokeSelectProjectsArgs, - ) - .await - .unwrap(); - projects_state.set_projects(projects).unwrap() + projects_store.load_projects().await; + queries_store.load_queries().await; }, ); view! { -
- +
+

Projects

@@ -53,17 +42,30 @@ pub fn Sidebar() -> impl IntoView {
} + children=|(project_id, project_details)| { + if project_details.contains(Drivers::PGSQL.as_ref()) { + view! { +
+ +
+ } + } else { + view! {
} + } + } /> +
-
-

Saved Queries

-
- +
}> +
+

Saved Queries

+
+ +
-
+
} } diff --git a/src/sidebar/mod.rs b/src/sidebar/mod.rs index 35127ed..6451be4 100644 --- a/src/sidebar/mod.rs +++ b/src/sidebar/mod.rs @@ -1,8 +1,4 @@ pub mod index; -pub mod project; pub mod queries; pub mod query; -pub mod schema; -pub mod schemas; -pub mod table; -pub mod tables; + diff --git a/src/sidebar/project.rs b/src/sidebar/project.rs deleted file mode 100644 index b86c47c..0000000 --- a/src/sidebar/project.rs +++ /dev/null @@ -1,68 +0,0 @@ -use leptos::*; - -use crate::store::{active_project::ActiveProjectStore, projects::ProjectsStore}; - -use super::schemas::Schemas; - -#[component] -pub fn Project(project: String) -> impl IntoView { - let projects_store = use_context::().unwrap(); - let active_project_store = use_context::().unwrap(); - let (show_schemas, set_show_schemas) = create_signal(false); - let delete_project = create_action(move |(projects_store, project): &(ProjectsStore, String)| { - let projects_store = *projects_store; - let project = project.clone(); - async move { - projects_store.delete_project(&project).await.unwrap(); - } - }); - - view! { -
-
- - -
-
- Loading...

} - }> - - { - let project = project.clone(); - view! { - - - - } - } - -
-
-
- } -} - diff --git a/src/sidebar/queries.rs b/src/sidebar/queries.rs index 42d502e..b06d070 100644 --- a/src/sidebar/queries.rs +++ b/src/sidebar/queries.rs @@ -1,21 +1,23 @@ -use crate::store::query::QueryStore; +use crate::store::queries::QueriesStore; use leptos::*; use super::query::Query; #[component] pub fn Queries() -> impl IntoView { - let query_state = use_context::().unwrap(); - let queries = create_resource( - move || query_state.0.get(), - move |_| async move { query_state.select_queries().await.unwrap() }, + let queries_store = expect_context::(); + let _ = create_resource( + move || queries_store.0.get(), + move |_| async move { + queries_store.load_queries().await; + }, ); view! { } + each=move || queries_store.0.get() + key=|(query_id, _)| query_id.clone() + children=move |(query_id, sql)| view! { } /> } } diff --git a/src/sidebar/query.rs b/src/sidebar/query.rs index 496f7aa..905df33 100644 --- a/src/sidebar/query.rs +++ b/src/sidebar/query.rs @@ -1,53 +1,53 @@ +use std::sync::Arc; + use leptos::*; use leptos_icons::*; -use crate::store::{query::QueryStore, tabs::TabsStore}; +use crate::store::{queries::QueriesStore, tabs::TabsStore}; #[component] -pub fn Query(key: String) -> impl IntoView { - let query_store = use_context::().unwrap(); - let tabs_store = use_context::().unwrap(); - let key_clone = key.clone(); - let splitted_key = create_memo(move |_| { - let key = key_clone.clone(); - - key - .split(':') - .map(|s| s.to_string()) - .collect::>() - }); +pub fn Query(query_id: String, sql: String) -> impl IntoView { + let query_id = Arc::new(query_id); + let sql = Arc::new(sql); + let query_store = expect_context::(); + let tabs_store = expect_context::(); view! {
} diff --git a/src/sidebar/schema.rs b/src/sidebar/schema.rs deleted file mode 100644 index 95daed5..0000000 --- a/src/sidebar/schema.rs +++ /dev/null @@ -1,43 +0,0 @@ -use leptos::*; - -use super::tables::Tables; - -#[component] -pub fn Schema(schema: String, project: String) -> impl IntoView { - let (show_tables, set_show_tables) = create_signal(false); - - view! { -
-
- {&schema} -
-
- "Loading..."

} - }> - - { - let schema = schema.clone(); - let project = project.clone(); - view! { - - - { - let schema = schema.clone(); - let project = project.clone(); - view! { } - } - - - } - } - -
-
-
- } -} - diff --git a/src/sidebar/schemas.rs b/src/sidebar/schemas.rs deleted file mode 100644 index 474de6f..0000000 --- a/src/sidebar/schemas.rs +++ /dev/null @@ -1,29 +0,0 @@ -use leptos::*; - -use super::schema::Schema; -use crate::store::projects::ProjectsStore; - -#[component] -pub fn Schemas(project: String) -> impl IntoView { - let projects_store = use_context::().unwrap(); - let project_clone = project.clone(); - let schemas = create_resource( - || {}, - move |_| { - let project = project.clone(); - async move { projects_store.connect(&project).await.unwrap() } - }, - ); - - view! { - } - } - /> - } -} - diff --git a/src/sidebar/table.rs b/src/sidebar/table.rs deleted file mode 100644 index 308d621..0000000 --- a/src/sidebar/table.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::sync::Arc; - -use futures::lock::Mutex; -use leptos::*; -use leptos_icons::*; - -use crate::store::{active_project::ActiveProjectStore, tabs::TabsStore}; - -#[component] -pub fn Table(table: (String, String), project: String, schema: String) -> impl IntoView { - let tabs_store = Arc::new(Mutex::new(use_context::().unwrap())); - let active_project = use_context::().unwrap(); - let query = create_action( - move |(schema, table, tabs_store): &(String, String, Arc>)| { - let tabs_store = tabs_store.clone(); - let project = project.clone(); - let schema = schema.clone(); - let table = table.clone(); - active_project.0.set(Some(project.clone())); - - async move { - tabs_store - .lock() - .await - .set_editor_value(&format!("SELECT * FROM {}.{} LIMIT 100;", schema, table)); - tabs_store.lock().await.run_query().await.unwrap() - } - }, - ); - - view! { -
- -
- -

{table.0}

-
-

{table.1}

-
- } -} - diff --git a/src/sidebar/tables.rs b/src/sidebar/tables.rs deleted file mode 100644 index 5127b05..0000000 --- a/src/sidebar/tables.rs +++ /dev/null @@ -1,33 +0,0 @@ -use leptos::*; - -use super::table::Table; -use crate::store::projects::ProjectsStore; - -#[component] -pub fn Tables(schema: String, project: String) -> impl IntoView { - let projects_store = use_context::().unwrap(); - let schema_clone = schema.clone(); - let project_clone = project.clone(); - let tables = create_resource( - || {}, - move |_| { - let schema = schema_clone.clone(); - let project = project_clone.clone(); - async move { - projects_store - .retrieve_tables(&project, &schema) - .await - .unwrap() - } - }, - ); - - view! { - } - /> - } -} - diff --git a/src/store/active_project.rs b/src/store/active_project.rs deleted file mode 100644 index 52a8339..0000000 --- a/src/store/active_project.rs +++ /dev/null @@ -1,17 +0,0 @@ -use leptos::{create_rw_signal, RwSignal}; - -#[derive(Clone, Debug)] -pub struct ActiveProjectStore(pub RwSignal>); - -impl Default for ActiveProjectStore { - fn default() -> Self { - Self::new() - } -} - -impl ActiveProjectStore { - #[must_use] - pub fn new() -> Self { - Self(create_rw_signal(None)) - } -} diff --git a/src/store/atoms.rs b/src/store/atoms.rs new file mode 100644 index 0000000..4e16d10 --- /dev/null +++ b/src/store/atoms.rs @@ -0,0 +1,33 @@ +use std::collections::VecDeque; + +use leptos::RwSignal; + +#[derive(Debug, Default, Clone)] +pub struct QueryPerformanceAtom { + pub id: usize, + pub message: String, +} + +impl QueryPerformanceAtom { + pub fn new(id: usize, sql: &str, query_time: f32) -> Self { + Self { + id, + message: format!( + "[{}]: {} is executed in {} ms", + chrono::Utc::now(), + sql, + query_time + ), + } + } +} + +pub type QueryPerformanceContext = RwSignal>; + +#[derive(Debug, Default, Clone)] +pub struct RunQueryAtom { + pub is_running: bool, +} + +pub type RunQueryContext = RwSignal; + diff --git a/src/store/mod.rs b/src/store/mod.rs index b9e9fac..1ee5f61 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -1,4 +1,5 @@ -pub mod active_project; +pub mod atoms; pub mod projects; -pub mod query; +pub mod queries; pub mod tabs; + diff --git a/src/store/projects.rs b/src/store/projects.rs index 7e976ea..486090a 100644 --- a/src/store/projects.rs +++ b/src/store/projects.rs @@ -1,19 +1,13 @@ use std::collections::BTreeMap; -use common::{ - enums::{Project, ProjectConnectionStatus}, - projects::postgresql::PostgresqlRelation, -}; -use leptos::{create_rw_signal, error::Result, RwSignal, SignalGet, SignalUpdate}; +use common::enums::Drivers; +use leptos::{RwSignal, SignalGet, SignalSet}; use tauri_sys::tauri::invoke; -use crate::invoke::{ - Invoke, InvokeDeleteProjectArgs, InvokePostgresConnectionArgs, InvokeSchemaRelationsArgs, - InvokeSchemaTablesArgs, -}; +use crate::invoke::{Invoke, InvokeProjectDbDeleteArgs, InvokeProjectDbInsertArgs}; #[derive(Clone, Copy, Debug)] -pub struct ProjectsStore(pub RwSignal>); +pub struct ProjectsStore(pub RwSignal>); impl Default for ProjectsStore { fn default() -> Self { @@ -24,171 +18,54 @@ impl Default for ProjectsStore { impl ProjectsStore { #[must_use] pub fn new() -> Self { - Self(create_rw_signal(BTreeMap::default())) + Self(RwSignal::default()) } - pub fn set_projects( - &self, - projects: Vec<(String, Project)>, - ) -> Result> { - self.0.update(|prev| { - // insert only if project does not exist - for (name, project) in projects.iter() { - if !prev.contains_key(name) { - prev.insert(name.clone(), project.clone()); - } - } - }); - Ok(self.0.get().clone()) + pub fn select_project_by_name(&self, project_id: &str) -> Option { + self.0.get().get(project_id).cloned() } - pub fn insert_project(&self, project: Project) -> Result<()> { - self.0.update(|prev| { - let project = match project { - Project::POSTGRESQL(project) => (project.name.clone(), Project::POSTGRESQL(project)), - }; - prev.insert(project.0, project.1); - }); - Ok(()) - } - - pub fn get_projects(&self) -> Result> { - let projects = self.0.get(); - let projects = projects.keys().cloned().collect::>(); - Ok(projects) - } - - pub fn create_project_connection_string(&self, project_name: &str) -> String { - let projects = self.0.get(); - let (_, project) = projects.get_key_value(project_name).unwrap(); - - match project { - Project::POSTGRESQL(project) => { - let driver = project.driver.clone(); - format!( - "user={} password={} host={} port={}", - driver.user, driver.password, driver.host, driver.port, - ) - } + pub fn select_driver_by_project(&self, project_id: Option<&str>) -> Drivers { + if project_id.is_none() { + return Drivers::PGSQL; } - } - - pub async fn connect(&self, project_name: &str) -> Result> { - let projects = self.0; - let _projects = projects.get(); - let project = _projects.get(project_name).unwrap(); - - match project { - Project::POSTGRESQL(project) => { - if project.connection_status == ProjectConnectionStatus::Connected { - return Ok(project.schemas.clone().unwrap()); - } - let schemas = self.postgresql_schema_selector(&project.name).await?; - projects.update(|prev| { - let project = prev.get_mut(project_name).unwrap(); - match project { - Project::POSTGRESQL(project) => { - project.schemas = Some(schemas.clone()); - project.connection_status = ProjectConnectionStatus::Connected; - } - } - }); - Ok(schemas) - } - } - } - pub async fn retrieve_tables( - &self, - project_name: &str, - schema: &str, - ) -> Result> { - let projects = self.0; - let _projects = projects.get(); - let project = _projects.get(project_name).unwrap(); + let project = self.select_project_by_name(project_id.unwrap()).unwrap(); + let driver = project.split(':').next().unwrap(); + let driver = driver.split('=').last(); - match project { - Project::POSTGRESQL(project) => { - if let Some(tables) = &project.tables { - if let Some(tables) = tables.get(schema) { - return Ok(tables.clone()); - } - } - - let (tables, relations) = self - .postgresql_table_selector(&project.name, schema) - .await - .unwrap(); - - projects.update(|prev| { - let project = prev.get_mut(project_name).unwrap(); - match project { - Project::POSTGRESQL(project) => { - project - .tables - .as_mut() - .unwrap_or(&mut BTreeMap::>::new()) - .insert(schema.to_string(), tables.clone()); - project.relations = Some(relations.clone()); - } - } - }); - - Ok(tables) - } + match driver { + Some("PGSQL") => Drivers::PGSQL, + _ => unreachable!(), } } - pub async fn delete_project(&self, project_name: &str) -> Result<()> { - invoke( - &Invoke::delete_project.to_string(), - &InvokeDeleteProjectArgs { project_name }, - ) - .await?; - let projects = self.0; - projects.update(|prev| { - prev.remove(project_name); - }); - Ok(()) + pub async fn load_projects(&self) { + let projects = invoke::<_, BTreeMap>(Invoke::ProjectDbSelect.as_ref(), &()) + .await + .unwrap(); + self.0.set(projects); } - async fn postgresql_schema_selector(&self, project_name: &str) -> Result> { - let connection_string = self.create_project_connection_string(project_name); - let mut schemas = invoke::<_, Vec>( - &Invoke::postgresql_connector.to_string(), - &InvokePostgresConnectionArgs { - project_name, - key: &connection_string, + pub async fn insert_project(&self, project_id: &str, project_details: &str) { + let _ = invoke::<_, ()>( + Invoke::ProjectDbInsert.as_ref(), + &InvokeProjectDbInsertArgs { + project_id, + project_details, }, ) - .await?; - schemas.sort(); - Ok(schemas) + .await; + self.load_projects().await; } - async fn postgresql_table_selector( - &self, - project_name: &str, - schema: &str, - ) -> Result<(Vec<(String, String)>, Vec)> { - let tables = invoke::<_, Vec<(String, String)>>( - &Invoke::select_schema_tables.to_string(), - &InvokeSchemaTablesArgs { - project_name, - schema, - }, - ) - .await?; - - let relations = invoke::<_, Vec>( - &Invoke::select_schema_relations.to_string(), - &InvokeSchemaRelationsArgs { - project_name, - schema, - }, + pub async fn delete_project(&self, project_id: &str) { + let _ = invoke::<_, ()>( + Invoke::ProjectDbDelete.as_ref(), + &InvokeProjectDbDeleteArgs { project_id }, ) - .await?; - - Ok((tables, relations)) + .await; + self.load_projects().await; } } + diff --git a/src/store/queries.rs b/src/store/queries.rs new file mode 100644 index 0000000..5a5229a --- /dev/null +++ b/src/store/queries.rs @@ -0,0 +1,58 @@ +use std::collections::BTreeMap; + +use common::enums::Drivers; +use leptos::*; +use tauri_sys::tauri::invoke; + +use crate::invoke::{Invoke, InvokeQueryDbDeleteArgs, InvokeQueryDbInsertArgs}; + +use super::tabs::TabsStore; + +#[derive(Clone, Copy, Debug)] +pub struct QueriesStore(pub RwSignal>); + +impl Default for QueriesStore { + fn default() -> Self { + Self::new() + } +} + +impl QueriesStore { + #[must_use] + pub fn new() -> Self { + Self(create_rw_signal(BTreeMap::new())) + } + + pub async fn load_queries(&self) { + let saved_queries = invoke::<_, BTreeMap>(Invoke::QueryDbSelect.as_ref(), &()) + .await + .unwrap(); + self.0.update(|prev| { + *prev = saved_queries.into_iter().collect(); + }); + } + + pub async fn insert_query(&self, project_id: &str, title: &str, driver: &Drivers) { + let tabs_store = expect_context::(); + let sql = tabs_store.select_active_editor_value(); + let _ = invoke::<_, ()>( + Invoke::QueryDbInsert.as_ref(), + &InvokeQueryDbInsertArgs { + query_id: &format!("{}:{}:{}", project_id, driver, title), + sql: &sql, + }, + ) + .await; + self.load_queries().await; + } + + pub async fn delete_query(&self, query_id: &str) { + let _ = invoke::<_, ()>( + Invoke::QueryDbDelete.as_ref(), + &InvokeQueryDbDeleteArgs { query_id }, + ) + .await; + self.load_queries().await; + } +} + diff --git a/src/store/query.rs b/src/store/query.rs deleted file mode 100644 index 52bcd24..0000000 --- a/src/store/query.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::collections::BTreeMap; - -use leptos::{error::Result, *}; -use tauri_sys::tauri::invoke; - -use crate::invoke::{ - Invoke, InvokeDeleteQueryArgs, InvokeInsertQueryArgs, InvokeSelectQueriesArgs, -}; - -use super::tabs::TabsStore; - -#[derive(Clone, Copy, Debug)] -pub struct QueryStore(pub RwSignal>); - -impl Default for QueryStore { - fn default() -> Self { - Self::new() - } -} - -impl QueryStore { - #[must_use] - pub fn new() -> Self { - Self(create_rw_signal(BTreeMap::new())) - } - - pub async fn select_queries(&self) -> Result> { - let saved_queries = invoke::<_, BTreeMap>( - &Invoke::select_queries.to_string(), - &InvokeSelectQueriesArgs, - ) - .await?; - - self.0.update(|prev| { - *prev = saved_queries.into_iter().collect(); - }); - Ok(self.0.get()) - } - - pub async fn insert_query(&self, key: &str, project_name: &str) -> Result<()> { - let tabs_store = use_context::().unwrap(); - let sql = tabs_store.select_active_editor_value(); - invoke( - &Invoke::insert_query.to_string(), - &InvokeInsertQueryArgs { - key: &format!("{}:{}", project_name, key), - sql: sql.as_str(), - }, - ) - .await?; - self.select_queries().await?; - Ok(()) - } - - pub async fn delete_query(&self, key: &str) -> Result<()> { - invoke( - &Invoke::delete_query.to_string(), - &InvokeDeleteQueryArgs { key }, - ) - .await?; - self.select_queries().await?; - Ok(()) - } -} diff --git a/src/store/tabs.rs b/src/store/tabs.rs index 6584b5e..a7b5b87 100644 --- a/src/store/tabs.rs +++ b/src/store/tabs.rs @@ -1,35 +1,32 @@ use std::{cell::RefCell, rc::Rc}; -use leptos::{ - create_rw_signal, error::Result, use_context, RwSignal, SignalGet, SignalSet, SignalUpdate, -}; +use common::enums::ProjectConnectionStatus; +use leptos::{create_rw_signal, expect_context, RwSignal, SignalGet, SignalSet, SignalUpdate}; use monaco::api::CodeEditor; +use rsql::set_running_query; use tauri_sys::tauri::invoke; use crate::{ - invoke::{Invoke, InvokeSqlResultArgs}, - query_editor::ModelCell, + dashboard::query_editor::ModelCell, + invoke::{Invoke, InvokePgsqlConnectorArgs, InvokePgsqlRunQueryArgs}, }; -use super::{active_project::ActiveProjectStore, projects::ProjectsStore, query::QueryStore}; +use super::atoms::{QueryPerformanceAtom, QueryPerformanceContext, RunQueryAtom, RunQueryContext}; -#[derive(Clone, Debug)] struct QueryInfo { query: String, - #[allow(dead_code)] - start_line: f64, - #[allow(dead_code)] - end_line: f64, + _start_line: f64, + _end_line: f64, } #[derive(Copy, Clone, Debug)] pub struct TabsStore { - pub active_tabs: RwSignal, pub selected_tab: RwSignal, + pub active_tabs: RwSignal, pub editors: RwSignal>, #[allow(clippy::type_complexity)] pub sql_results: RwSignal, Vec>)>>, - pub is_loading: RwSignal, + pub selected_projects: RwSignal>, } unsafe impl Send for TabsStore {} @@ -45,20 +42,20 @@ impl TabsStore { #[must_use] pub fn new() -> Self { Self { - active_tabs: create_rw_signal(1), selected_tab: create_rw_signal(String::from("0")), + active_tabs: create_rw_signal(1), editors: create_rw_signal(Vec::new()), sql_results: create_rw_signal(Vec::new()), - is_loading: create_rw_signal(false), + selected_projects: create_rw_signal(Vec::new()), } } - pub async fn run_query(&self) -> Result<()> { - self.is_loading.set(true); - let active_project = use_context::().unwrap(); - let active_project = active_project.0.get().unwrap(); - let projects_store = use_context::().unwrap(); - projects_store.connect(&active_project).await?; + #[set_running_query] + pub async fn run_query(&self) { + let project_ids = self.selected_projects.get(); + let project_id = project_ids + .get(self.convert_selected_tab_to_index()) + .unwrap(); let active_editor = self.select_active_editor(); let position = active_editor .borrow() @@ -71,16 +68,15 @@ impl TabsStore { let sql = self .find_query_for_line(&sql, position.line_number()) .unwrap(); - let (cols, rows, elasped) = invoke::<_, (Vec, Vec>, f32)>( - &Invoke::select_sql_result.to_string(), - &InvokeSqlResultArgs { - project_name: &active_project, + let (cols, rows, query_time) = invoke::<_, (Vec, Vec>, f32)>( + &Invoke::PgsqlRunQuery.to_string(), + &InvokePgsqlRunQueryArgs { + project_id, sql: &sql.query, }, ) - .await?; - let sql_timer = use_context::>().unwrap(); - sql_timer.set(elasped); + .await + .unwrap(); self.sql_results.update(|prev| { let index = self.convert_selected_tab_to_index(); match prev.get_mut(index) { @@ -88,44 +84,78 @@ impl TabsStore { None => prev.push((cols, rows)), } }); - self.is_loading.set(false); - Ok(()) - } - - pub fn load_query(&self, key: &str) -> Result<()> { - let active_project = use_context::().unwrap(); - let splitted_key = key.split(':').collect::>(); - active_project.0.set(Some(splitted_key[0].to_string())); - let query_store = use_context::().unwrap(); - let query_store = query_store.0.get(); - let query = query_store.get(key).unwrap(); - self.set_editor_value(query); - Ok(()) + let qp_store = expect_context::(); + qp_store.update(|prev| { + let new = QueryPerformanceAtom::new(prev.len(), &sql.query, query_time); + prev.push_front(new); + }); } - pub fn select_active_editor_sql_result(&self) -> Option<(Vec, Vec>)> { - self - .sql_results - .get() - .get(self.convert_selected_tab_to_index()) - .cloned() + // TODO: Need to be more generic if we want to support other databases + pub async fn load_query(&self, query_id: &str, sql: &str) { + let splitted_key = query_id.split(':').collect::>(); + let selected_projects = self.selected_projects.get(); + let project_id = selected_projects.get(self.convert_selected_tab_to_index()); + if !self.selected_projects.get().is_empty() + && project_id.is_some_and(|id| id.as_str() != splitted_key[0]) + { + self.add_tab(&splitted_key[0]); + } + self.set_editor_value(sql); + self.selected_projects.update(|prev| { + let index = self.convert_selected_tab_to_index(); + match prev.get_mut(index) { + Some(project) => *project = splitted_key[0].to_string(), + None => prev.push(splitted_key[0].to_string()), + } + }); + let _ = invoke::<_, ProjectConnectionStatus>( + Invoke::PgsqlConnector.as_ref(), + &InvokePgsqlConnectorArgs { + project_id: splitted_key[0], + key: None, + }, + ) + .await; + self.run_query().await; } pub fn add_editor(&mut self, editor: Rc>>) { self.editors.update(|prev| { prev.push(editor); }); - self.sql_results.update(|prev| { - prev.push((Vec::new(), Vec::new())); + } + + pub fn add_tab(&self, project_id: &str) { + if self.editors.get().len() == 1 && self.selected_projects.get().is_empty() { + self.selected_projects.update(|prev| { + prev.push(project_id.to_string()); + }); + return; + } + + self.active_tabs.update(|prev| { + *prev += 1; + }); + + self.selected_tab.update(|prev| { + *prev = (self.active_tabs.get() - 1).to_string(); + }); + + self.selected_projects.update(|prev| { + prev.push(project_id.to_string()); }); } - #[allow(dead_code)] - pub fn remove_editor(&mut self, index: usize) { + pub fn close_tab(&self, index: usize) { if self.active_tabs.get() == 1 { return; } + self.selected_tab.update(|prev| { + *prev = (index - 1).to_string(); + }); + self.active_tabs.update(|prev| { *prev -= 1; }); @@ -133,10 +163,14 @@ impl TabsStore { self.editors.update(|prev| { prev.remove(index); }); + } - self.sql_results.update(|prev| { - prev.remove(index); - }); + pub fn select_active_editor_sql_result(&self) -> Option<(Vec, Vec>)> { + self + .sql_results + .get() + .get(self.convert_selected_tab_to_index()) + .cloned() } pub fn select_active_editor(&self) -> ModelCell { @@ -176,11 +210,10 @@ impl TabsStore { .set_value(value); } - pub(self) fn convert_selected_tab_to_index(&self) -> usize { + pub fn convert_selected_tab_to_index(&self) -> usize { self.selected_tab.get().parse::().unwrap() } - // TODO: improve this pub(self) fn find_query_for_line(&self, queries: &str, line_number: f64) -> Option { let mut start_line = 1f64; let mut end_line = 1f64; @@ -197,8 +230,8 @@ impl TabsStore { if line_number >= start_line && line_number < end_line { return Some(QueryInfo { query: current_query.clone(), - start_line, - end_line: end_line - 1f64, + _start_line: start_line, + _end_line: end_line - 1f64, }); } start_line = end_line;