diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 537f614..926a5f2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest # add rust nightly container: - image: rust:latest + image: rustlang/rust:nightly steps: - uses: actions/checkout@v3 diff --git a/Cargo.toml b/Cargo.toml index 855de31..fea1337 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-sql-gui-ui" -version = "1.0.0-alpha.2" +version = "1.0.0-alpha.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -16,8 +16,9 @@ leptos_icons = { version = "0.1.0", features = ["HiBars4OutlineLg", "HiCircleSta serde_json = "1.0.108" wasm-bindgen-futures = "0.4.39" monaco = "0.4.0" -# tauri = { version = "1.5.3", features = ["dialog"] } thaw = "0.1.3" +common = { path = "common" } + [workspace] -members = ["src-tauri"] +members = ["src-tauri", "common"] diff --git a/common/Cargo.toml b/common/Cargo.toml new file mode 100644 index 0000000..7f65b95 --- /dev/null +++ b/common/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "common" +version = "1.0.0-alpha.3" +edition = "2021" + +[dependencies] +serde = "1.0.193" diff --git a/common/src/lib.rs b/common/src/lib.rs new file mode 100644 index 0000000..36df406 --- /dev/null +++ b/common/src/lib.rs @@ -0,0 +1 @@ +pub mod project; diff --git a/common/src/project.rs b/common/src/project.rs new file mode 100644 index 0000000..f3787ee --- /dev/null +++ b/common/src/project.rs @@ -0,0 +1,10 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct ProjectDetails { + pub name: String, + pub user: String, + pub password: String, + pub host: String, + pub port: String, +} diff --git a/public/leptos.svg b/public/leptos.svg deleted file mode 100644 index 7fc2154..0000000 --- a/public/leptos.svg +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/public/tauri.svg b/public/tauri.svg deleted file mode 100644 index 31b62c9..0000000 --- a/public/tauri.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 39393f6..be672f6 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.2" +version = "1.0.0-alpha.3" description = "PostgreSQL GUI written in Rust" authors = ["you"] license = "" @@ -20,6 +20,7 @@ tokio = "1.34.0" tokio-postgres = "0.7.10" chrono = "0.4.31" sled = "0.34.7" +common = { path = "../common" } diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png index b048cc6..0057a36 100644 Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png index dbb9492..7e3352a 100644 Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png index d11aac0..73ac039 100644 Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png index 1663ee4..018eb2c 100644 Binary files a/src-tauri/icons/Square107x107Logo.png and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png index 0c5ece3..3d46dc1 100644 Binary files a/src-tauri/icons/Square142x142Logo.png and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png index 3a53eda..488b6ec 100644 Binary files a/src-tauri/icons/Square150x150Logo.png and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png index 46fd469..23df99d 100644 Binary files a/src-tauri/icons/Square284x284Logo.png and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png index ccb98be..c4342b7 100644 Binary files a/src-tauri/icons/Square30x30Logo.png and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png index e6b70ef..f61d3d2 100644 Binary files a/src-tauri/icons/Square310x310Logo.png and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png index c370e90..2ff6aa3 100644 Binary files a/src-tauri/icons/Square44x44Logo.png and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png index 2c416ff..10867c5 100644 Binary files a/src-tauri/icons/Square71x71Logo.png and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png index 4900938..d9252dd 100644 Binary files a/src-tauri/icons/Square89x89Logo.png and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png index 9b41314..d51b7b0 100644 Binary files a/src-tauri/icons/StoreLogo.png and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns index 3052f73..2bf8c51 100644 Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico index 57e918f..3aec0a6 100644 Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png index e18d7b1..7108f1f 100644 Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0e16467..cac21e8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -9,18 +9,17 @@ mod utils; use constant::{PROJECT_DB_PATH, QUERY_DB_PATH}; use postgres::{pg_connector, select_schema_tables, select_sql_result}; -use project_db::{delete_project, select_project_details, select_projects}; +use project_db::{delete_project, insert_project, select_projects}; use query_db::{delete_query, insert_query, select_queries}; use sled::Db; -use std::sync::Arc; +use std::{collections::BTreeMap, sync::Arc}; use tauri::Manager; use tokio::sync::Mutex; use tokio_postgres::Client; use utils::create_or_open_local_db; pub struct AppState { - pub connection_strings: Arc>, - pub client: Arc>>, + pub client: Arc>>>, pub project_db: Arc>>, pub query_db: Arc>>, } @@ -28,8 +27,7 @@ pub struct AppState { impl Default for AppState { fn default() -> Self { Self { - connection_strings: Arc::new(Mutex::new(String::new())), - client: Arc::new(Mutex::new(None)), + client: Arc::new(Mutex::new(Some(BTreeMap::new()))), project_db: Arc::new(Mutex::new(None)), query_db: Arc::new(Mutex::new(None)), } @@ -64,10 +62,10 @@ fn main() { .invoke_handler(tauri::generate_handler![ delete_project, delete_query, + insert_project, insert_query, pg_connector, select_projects, - select_project_details, select_queries, select_schema_tables, select_sql_result, diff --git a/src-tauri/src/postgres.rs b/src-tauri/src/postgres.rs index 9957f81..cd50c38 100644 --- a/src-tauri/src/postgres.rs +++ b/src-tauri/src/postgres.rs @@ -30,20 +30,21 @@ pub async fn pg_connector(project: &str, key: &str, app: AppHandle) -> Result, ) -> Result> { - let client = app_state.client.lock().await; - let client = client.as_ref().unwrap(); + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project).unwrap(); let tables = client .query( r#" @@ -70,12 +71,12 @@ pub async fn select_schema_tables( #[tauri::command] pub async fn select_sql_result( + project: &str, sql: String, app_state: State<'_, AppState>, ) -> Result<(Vec, Vec>)> { - let client = app_state.client.lock().await; - let client = client.as_ref().unwrap(); - + let clients = app_state.client.lock().await; + let client = clients.as_ref().unwrap().get(project).unwrap(); let rows = client.query(sql.as_str(), &[]).await.unwrap(); if rows.is_empty() { diff --git a/src-tauri/src/project_db.rs b/src-tauri/src/project_db.rs index fec57d9..8e7b910 100644 --- a/src-tauri/src/project_db.rs +++ b/src-tauri/src/project_db.rs @@ -1,62 +1,62 @@ -use serde::Serialize; +use common::project::ProjectDetails; use tauri::{Result, State}; use crate::AppState; -#[derive(Default, Serialize)] -pub struct ProjectDetails { - pub user: String, - pub password: String, - pub host: String, - pub port: String, -} - #[tauri::command] -pub async fn select_projects(app_state: State<'_, AppState>) -> Result> { +pub async fn select_projects(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, _) = r.unwrap(); - String::from_utf8(project.to_vec()).unwrap() + let (project, connection_string) = r.unwrap(); + let project = String::from_utf8(project.to_vec()).unwrap(); + let connection_string = connection_string.to_vec(); + let connection_string = String::from_utf8(connection_string).unwrap(); + let connection_string = connection_string.split(' ').collect::>(); + + let mut project_details = ProjectDetails { + name: project, + ..Default::default() + }; + + for connection_string in connection_string { + let connection_string = connection_string.split('=').collect::>(); + let key = connection_string[0]; + let value = connection_string[1]; + + match key { + "user" => project_details.user = value.to_string(), + "password" => project_details.password = value.to_string(), + "host" => project_details.host = value.to_string(), + "port" => project_details.port = value.to_string(), + _ => (), + } + } + + project_details }) - .collect::>(); - projects.sort(); + .collect::>(); + projects.sort_by(|a, b| a.name.cmp(&b.name)); Ok(projects) } #[tauri::command] -pub async fn select_project_details( - project: String, +pub async fn insert_project( + project: ProjectDetails, app_state: State<'_, AppState>, ) -> Result { - let db = app_state.project_db.lock().await; - let db = db.clone().unwrap(); - let connection_string = db.get(project).unwrap(); - let mut project_details = ProjectDetails::default(); - - if let Some(connection_string) = connection_string { - let connection_string = connection_string.to_vec(); - let connection_string = String::from_utf8(connection_string).unwrap(); - let connection_string = connection_string.split(' ').collect::>(); - - for connection_string in connection_string { - let connection_string = connection_string.split('=').collect::>(); - let key = connection_string[0]; - let value = connection_string[1]; - - match key { - "user" => project_details.user = value.to_string(), - "password" => project_details.password = value.to_string(), - "host" => project_details.host = value.to_string(), - "port" => project_details.port = value.to_string(), - _ => (), - } - } - } - Ok(project_details) + let project_db = app_state.project_db.lock().await; + let ref mut db = project_db.clone().unwrap(); + let connection_string = format!( + "user={} password={} host={} port={}", + project.user, project.password, project.host, project.port, + ); + db.insert(project.name.clone(), connection_string.as_str()) + .unwrap(); + Ok(project) } #[tauri::command] diff --git a/src-tauri/src/query_db.rs b/src-tauri/src/query_db.rs index 88e9027..3e17891 100644 --- a/src-tauri/src/query_db.rs +++ b/src-tauri/src/query_db.rs @@ -1,16 +1,9 @@ use std::collections::BTreeMap; -use serde::Serialize; use tauri::{AppHandle, Manager, Result, State}; use crate::AppState; -#[derive(Default, Serialize)] -pub struct QueryDetails { - pub title: String, - pub sql: String, -} - #[tauri::command] pub async fn insert_query(key: &str, sql: &str, app: AppHandle) -> Result<()> { let app_state = app.state::(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1a7f100..d0a4b09 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "RSQL", - "version": "1.0.0-alpha.1" + "version": "1.0.0-alpha.3" }, "tauri": { "allowlist": { diff --git a/src/app.rs b/src/app.rs index 35c33fc..4cc3967 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,20 +2,25 @@ use std::vec; use crate::{ enums::QueryTableLayout, - layout::layout, - query_editor::query_editor, - query_table::query_table, - store::{db::DBStore, editor::EditorState, query::QueryState}, + layout, query_editor, query_table, + store::{ + active_project::ActiveProjectStore, editor::EditorStore, projects::ProjectsStore, + query::QueryStore, + }, }; use leptos::*; pub fn app() -> impl IntoView { - provide_context(DBStore::default()); - provide_context(EditorState::default()); - provide_context(QueryState::default()); + provide_context(EditorStore::default()); + provide_context(QueryStore::default()); + provide_context(ProjectsStore::default()); provide_context(create_rw_signal(QueryTableLayout::Grid)); + provide_context(ActiveProjectStore::default()); - layout(Children::to_children(move || { - Fragment::new(vec![query_editor().into_view(), query_table().into_view()]) + layout::component(Children::to_children(move || { + Fragment::new(vec![ + query_editor::component().into_view(), + query_table::component().into_view(), + ]) })) } diff --git a/src/db_connector.rs b/src/db_connector.rs deleted file mode 100644 index 3c3ca57..0000000 --- a/src/db_connector.rs +++ /dev/null @@ -1,164 +0,0 @@ -use crate::store::{db::DBStore, query::QueryState}; -use leptos::{html::*, *}; -use leptos_use::{use_document, use_event_listener}; -use thaw::{Modal, ModalFooter, ModalProps}; - -pub fn db_connector() -> impl IntoView { - let show = create_rw_signal(false); - let _ = use_event_listener(use_document(), ev::keydown, move |event| { - if event.key() == "Escape" { - show.set(false); - } - }); - let (query_title, set_query_title) = create_signal(String::new()); - let db_state = use_context::().unwrap(); - let connect = create_action(move |db: &DBStore| { - let db_clone = *db; - async move { db_clone.connect().await } - }); - let query_state = use_context::().unwrap(); - let run_query = create_action(move |query_state: &QueryState| { - let query_state = *query_state; - async move { query_state.run_query().await } - }); - let insert_query = create_action(move |(query_db, key): &(QueryState, String)| { - let query_db_clone = *query_db; - let key = key.clone(); - async move { - query_db_clone.insert_query(key.as_str()).await.unwrap(); - } - }); - - header() - .classes("flex flex-row justify-between p-4 gap-2 border-b-1 border-neutral-200") - .child( - div() - .child(Modal(ModalProps { - show, - title: MaybeSignal::derive(move || String::from("Save query!")), - children: Children::to_children(move || Fragment::new(vec![ - div() - .child( - input() - .classes("border-1 border-neutral-200 p-1 rounded-md w-full") - .prop("type", "text") - .prop("placeholder", "Add query name..") - .prop("value", query_title) - .on(ev::input, move |e| { - set_query_title(event_target_value(&e)); - }) - ) - .into_view() - ])), - modal_footer: Some(ModalFooter { - children: ChildrenFn::to_children(move || Fragment::new(vec![ - div() - .classes("flex gap-2") - .attr("style", "justify-content: flex-end") - .child( - button() - .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md") - .on(ev::click, move |_| { - insert_query.dispatch((query_state, query_title())); - show.set(false); - }) - .child("Save") - ) - .child( - button() - .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md") - .on(ev::click, move |_| { - show.set(false) - }) - .child("Cancel") - ).into_view(), - ])), - }) - })) - .classes("flex flex-row gap-2") - .child( - input() - .classes("border-1 border-neutral-200 p-1 rounded-md") - .prop("type", "text") - .prop("placeholder", "project") - .prop("value", move || db_state.project.get()) - .on(ev::input, move |e| { - db_state.project.update(|prev| { - *prev = event_target_value(&e); - }); - }), - ) - .child( - input() - .classes("border-1 border-neutral-200 p-1 rounded-md") - .prop("type", "text") - .prop("value", move || db_state.db_user.get()) - .prop("placeholder", "username") - .on(ev::input, move |e| { - db_state.db_user.set(event_target_value(&e)); - }), - ) - .child( - input() - .classes( "border-1 border-neutral-200 p-1 rounded-md") - .prop("type", "password") - .prop("value", move || db_state.db_password.get()) - .prop("placeholder", "password") - .on(ev::input, move |e| { - db_state.db_password.set(event_target_value(&e)); - }), - ) - .child( - input() - .classes("border-1 border-neutral-200 p-1 rounded-md") - .prop("type", "text") - .prop("value", move || db_state.db_host.get()) - .prop("placeholder", "host") - .on(ev::input, move |e| { - db_state.db_host.set(event_target_value(&e)); - }), - ) - .child( - input() - .classes("border-1 border-neutral-200 p-1 rounded-md") - .prop("type", "text") - .prop("value", move || db_state.db_port.get()) - .prop("placeholder", "port") - .on(ev::input, move |e| { - db_state.db_port.set(event_target_value(&e)); - }), - ), - ) - .child( - div() - .classes("flex flex-row gap-2") - .child( - button() - .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md") - .on(ev::click, move |_| { - show.set(true) - }) - .child("Save Query"), - ) - .child( - button() - .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md") - .on(ev::click, move |_| run_query.dispatch(query_state)) - .child("Query"), - ) - .child( - button() - .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md disabled:opacity-50").prop("disabled", move || { - db_state.is_connecting.get() - || db_state.db_host.get().is_empty() - || db_state.db_port.get().is_empty() - || db_state.db_user.get().is_empty() - || db_state.db_password.get().is_empty() - }) - .on(ev::click, move |_| { - connect.dispatch(db_state); - }) - .child("Connect"), - ), - ) -} diff --git a/src/enums.rs b/src/enums.rs index 7f843c7..3be19d5 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -1,5 +1,14 @@ +use serde::{Deserialize, Serialize}; + #[derive(Debug, Clone, PartialEq, Eq)] pub enum QueryTableLayout { Grid, Records, } + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum ProjectConnectionStatus { + Connected, + #[default] + Disconnected, +} diff --git a/src/footer.rs b/src/footer.rs index 2c79e8d..98de21e 100644 --- a/src/footer.rs +++ b/src/footer.rs @@ -1,13 +1,29 @@ use leptos::{html::*, *}; use leptos_icons::*; -use crate::enums::QueryTableLayout; +use crate::{enums::QueryTableLayout, store::active_project::ActiveProjectStore}; -pub fn footer_layout() -> impl IntoView { +pub fn component() -> impl IntoView { let table_view = use_context::>().unwrap(); + let acitve_project = use_context::().unwrap(); footer() - .classes("flex flex-row justify-end items-center h-10 bg-gray-50 px-4") + .classes("flex flex-row justify-between items-center h-10 bg-gray-50 px-4") + .child(Show(ShowProps { + children: ChildrenFn::to_children(move || { + Fragment::new(vec![div() + .classes("flex flex-row items-center gap-1 text-xs") + .child(p().child("Selected project:")) + .child( + p() + .classes("font-semibold") + .child(move || acitve_project.0.get()), + ) + .into_view()]) + }), + when: move || acitve_project.0.get().is_some(), + fallback: ViewFn::from(div), + })) .child( div() .classes("flex flex-row gap-1") diff --git a/src/grid_view.rs b/src/grid_view.rs index a3ac14a..92f9943 100644 --- a/src/grid_view.rs +++ b/src/grid_view.rs @@ -1,32 +1,32 @@ -use leptos::{html::*, leptos_dom::Each, *}; +use leptos::{html::*, *}; -use crate::store::query::QueryState; +use crate::store::query::QueryStore; -pub fn grid_view() -> impl IntoView { - let query_state = use_context::().unwrap(); +pub fn component() -> impl IntoView { + let query_state = use_context::().unwrap(); table() .classes("table-auto w-full") .child( thead() .classes("sticky top-0 bg-white") - .child(tr().classes("bg-gray-100").child(Each::new( - move || query_state.sql_result.get().unwrap().0.clone(), - move |n| n.clone(), - move |col| th().classes("text-xs px-4").child(col), - ))), + .child(tr().classes("bg-gray-100").child(For(ForProps { + each: move || query_state.sql_result.get().unwrap().0.clone(), + key: |n| n.clone(), + children: move |col| th().classes("text-xs px-4").child(col), + }))), ) - .child(tbody().child(Each::new( - move || query_state.sql_result.get().unwrap().1.clone(), - move |n| n.clone(), - move |row| { + .child(tbody().child(For(ForProps { + each: move || query_state.sql_result.get().unwrap().1.clone(), + key: |n| n.clone(), + children: move |row| { tr() .classes("hover:bg-gray-100 divide-x divide-gray-200") - .child(Each::new( - move || row.clone(), - move |n| n.clone(), - move |cell| td().classes("px-4 text-xs cursor:pointer").child(cell), - )) + .child(For(ForProps { + each: move || row.clone(), + key: |n| n.clone(), + children: move |cell| td().classes("px-4 text-xs cursor:pointer").child(cell), + })) }, - ))) + }))) } diff --git a/src/invoke.rs b/src/invoke.rs index d0ea636..0f445f4 100644 --- a/src/invoke.rs +++ b/src/invoke.rs @@ -1,15 +1,16 @@ use std::fmt::Display; +use common::project::ProjectDetails; use serde::{Deserialize, Serialize}; #[allow(non_camel_case_types)] pub enum Invoke { delete_project, delete_query, + insert_project, insert_query, pg_connector, select_projects, - select_project_details, select_queries, select_schema_tables, select_sql_result, @@ -20,10 +21,10 @@ impl Display for Invoke { 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::pg_connector => write!(f, "pg_connector"), Invoke::select_projects => write!(f, "select_projects"), - Invoke::select_project_details => write!(f, "select_project_details"), Invoke::select_queries => write!(f, "select_queries"), Invoke::select_schema_tables => write!(f, "select_schema_tables"), Invoke::select_sql_result => write!(f, "select_sql_result"), @@ -38,21 +39,23 @@ pub struct InvokePostgresConnectionArgs { } #[derive(Serialize, Deserialize)] -pub struct InvokeTablesArgs { +pub struct InvokeSchemaTablesArgs { + pub project: String, pub schema: String, } #[derive(Serialize, Deserialize)] -pub struct InvokeQueryArgs { +pub struct InvokeSqlResultArgs { + pub project: String, pub sql: String, } #[derive(Serialize, Deserialize)] -pub struct InvokeProjectsArgs; +pub struct InvokeSelectProjectsArgs; #[derive(Serialize, Deserialize)] -pub struct InvokeProjectDetailsArgs { - pub project: String, +pub struct InvokeInsertProjectArgs { + pub project: ProjectDetails, } #[derive(Serialize, Deserialize)] diff --git a/src/layout.rs b/src/layout.rs index 586afe1..471cb6e 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,12 +1,14 @@ -use crate::{db_connector::db_connector, footer::footer_layout, sidebar::sidebar}; +use crate::{footer, sidebar}; use leptos::{html::*, *}; -pub fn layout(children: Children) -> impl IntoView { - div().classes("flex h-screen").child(sidebar()).child( - div() - .classes("flex flex-col flex-1 overflow-hidden") - .child(db_connector()) - .child(main().classes("flex-1 overflow-y-scroll").child(children())) - .child(footer_layout()), - ) +pub fn component(children: Children) -> impl IntoView { + div() + .classes("flex h-screen") + .child(sidebar::index::component()) + .child( + div() + .classes("flex flex-col flex-1 overflow-hidden") + .child(main().classes("flex-1 overflow-y-scroll").child(children())) + .child(footer::component()), + ) } diff --git a/src/main.rs b/src/main.rs index a5ffd15..34cf8d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,15 @@ mod app; -mod db_connector; mod enums; mod footer; mod grid_view; mod invoke; mod layout; -mod queries; +mod modals; mod query_editor; mod query_table; mod record_view; mod sidebar; mod store; -mod tables; mod wasm_functions; use app::*; diff --git a/src/modals/connection.rs b/src/modals/connection.rs new file mode 100644 index 0000000..817eb27 --- /dev/null +++ b/src/modals/connection.rs @@ -0,0 +1,116 @@ +use common::project::ProjectDetails; +use leptos::{html::*, *}; +use thaw::{Modal, ModalFooter, ModalProps}; + +use crate::{ + invoke::{Invoke, InvokeInsertProjectArgs}, + store::projects::ProjectsStore, + wasm_functions::invoke, +}; + +pub fn component(show: RwSignal) -> impl IntoView { + let projects_store = use_context::().unwrap(); + 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: &ProjectDetails| { + let project_details = project_details.clone(); + async move { + let args = serde_wasm_bindgen::to_value(&InvokeInsertProjectArgs { + project: project_details, + }) + .unwrap(); + let project = invoke(&Invoke::insert_project.to_string(), args).await; + let project = serde_wasm_bindgen::from_value::(project).unwrap(); + projects_store.insert_project(project).unwrap(); + show.set(false); + } + }); + + Modal(ModalProps { + show, + title: MaybeSignal::Static(String::from("Add new project")), + children: Children::to_children(move || { + Fragment::new(vec![div() + .classes("flex flex-col gap-2") + .child( + input() + .classes("border-1 border-neutral-200 p-1 rounded-md") + .prop("type", "text") + .prop("placeholder", "project") + .prop("value", project) + .on(ev::input, move |e| set_project(event_target_value(&e))), + ) + .child( + input() + .classes("border-1 border-neutral-200 p-1 rounded-md") + .prop("type", "text") + .prop("value", db_user) + .prop("placeholder", "username") + .on(ev::input, move |e| set_db_user(event_target_value(&e))), + ) + .child( + input() + .classes("border-1 border-neutral-200 p-1 rounded-md") + .prop("type", "password") + .prop("value", db_password) + .prop("placeholder", "password") + .on(ev::input, move |e| set_db_password(event_target_value(&e))), + ) + .child( + input() + .classes("border-1 border-neutral-200 p-1 rounded-md") + .prop("type", "text") + .prop("value", db_host) + .prop("placeholder", "host") + .on(ev::input, move |e| set_db_host(event_target_value(&e))), + ) + .child( + input() + .classes("border-1 border-neutral-200 p-1 rounded-md") + .prop("type", "text") + .prop("value", db_port) + .prop("placeholder", "port") + .on(ev::input, move |e| set_db_port(event_target_value(&e))), + ) + .into_view()]) + }), + modal_footer: Some(ModalFooter { + children: ChildrenFn::to_children(move || { + Fragment::new(vec![div() + .classes("flex gap-2 justify-end") + .child( + button() + .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md") + .child("Add") + .prop("disabled", move || { + project().is_empty() + || db_user().is_empty() + || db_password().is_empty() + || db_host().is_empty() + || db_port().is_empty() + }) + .on(ev::click, move |_| { + let project_details = ProjectDetails { + name: project(), + user: db_user(), + password: db_password(), + host: db_host(), + port: db_port(), + }; + save_project.dispatch(project_details); + }), + ) + .child( + button() + .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md") + .child("Cancel") + .on(ev::click, move |_| show.set(false)), + ) + .into_view()]) + }), + }), + }) +} diff --git a/src/modals/custom_query.rs b/src/modals/custom_query.rs new file mode 100644 index 0000000..9b200c8 --- /dev/null +++ b/src/modals/custom_query.rs @@ -0,0 +1,81 @@ +use leptos::{html::*, *}; +use thaw::{Modal, ModalFooter, ModalProps}; + +use crate::store::{projects::ProjectsStore, query::QueryStore}; + +pub fn component(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 (project, set_project) = create_signal(String::new()); + let insert_query = create_action( + move |(query_db, key, project): &(QueryStore, String, String)| { + let query_db_clone = *query_db; + let key = key.clone(); + let project = project.clone(); + async move { + query_db_clone.insert_query(&key, &project).await.unwrap(); + } + }, + ); + + Modal(ModalProps { + show, + title: MaybeSignal::Static(String::from("Save query!")), + children: Children::to_children(move || { + Fragment::new(vec![div() + .classes("flex flex-col gap-2") + .child( + select() + .classes("border-1 border-neutral-200 p-1 rounded-md w-full bg-white appearance-none") + .child(For(ForProps { + each: move || projects.get(), + key: |project| project.clone(), + children: move |p| { + option() + .prop("value", &p) + .prop("selected", p == project()) + .child(&p) + }, + })) + .on(ev::change, move |e| { + set_project(event_target_value(&e)); + }), + ) + .child( + input() + .classes("border-1 border-neutral-200 p-1 rounded-md w-full") + .prop("type", "text") + .prop("placeholder", "Add query name..") + .prop("value", query_title) + .on(ev::input, move |e| { + set_query_title(event_target_value(&e)); + }), + ) + .into_view()]) + }), + modal_footer: Some(ModalFooter { + children: ChildrenFn::to_children(move || { + Fragment::new(vec![div() + .classes("flex gap-2 justify-end") + .child( + button() + .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md") + .on(ev::click, move |_| { + insert_query.dispatch((query_store, query_title(), project())); + show.set(false); + }) + .child("Save"), + ) + .child( + button() + .classes("px-4 py-2 border-1 border-neutral-200 hover:bg-neutral-200 rounded-md") + .on(ev::click, move |_| show.set(false)) + .child("Cancel"), + ) + .into_view()]) + }), + }), + }) +} diff --git a/src/modals/mod.rs b/src/modals/mod.rs new file mode 100644 index 0000000..d34fe1c --- /dev/null +++ b/src/modals/mod.rs @@ -0,0 +1,2 @@ +pub mod connection; +pub mod custom_query; diff --git a/src/queries.rs b/src/queries.rs deleted file mode 100644 index 56821db..0000000 --- a/src/queries.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::store::query::QueryState; -use leptos::{html::*, *}; -use leptos_icons::*; - -pub fn queries() -> impl IntoView { - let query_state = use_context::().unwrap(); - create_resource( - || {}, - move |_| async move { - query_state.select_queries().await.unwrap(); - }, - ); - - move || { - query_state - .saved_queries - .get() - .into_iter() - .enumerate() - .map(|(idx, (key, _))| { - div() - .prop("key", idx) - .classes("flex flex-row justify-between items-center") - .child( - button() - .classes("hover:font-semibold") - .child( - div() - .classes("flex flex-row items-center gap-1") - .child(Icon(IconProps { - icon: MaybeSignal::derive(|| Icon::from(HiIcon::HiCircleStackOutlineLg)), - width: Some(MaybeSignal::derive(|| String::from("12"))), - height: Some(MaybeSignal::derive(|| String::from("12"))), - class: None, - style: None, - })) - .child(&key), - ) - .on(ev::click, { - let key = key.clone(); - move |_| { - query_state.load_query(&key); - } - }), - ) - .child( - button() - .classes("px-2 rounded-full hover:bg-gray-200") - .child("-") - .on(ev::click, move |_| { - let key = key.clone(); - spawn_local(async move { - query_state.delete_query(&key).await.unwrap(); - }) - }), - ) - }) - .collect_view() - } -} diff --git a/src/query_editor.rs b/src/query_editor.rs index 3462134..34ea823 100644 --- a/src/query_editor.rs +++ b/src/query_editor.rs @@ -1,26 +1,29 @@ use std::{cell::RefCell, rc::Rc}; use leptos::{html::*, *}; -use leptos_use::use_event_listener; +use leptos_use::{use_document, use_event_listener}; use monaco::{ api::{CodeEditor, CodeEditorOptions}, sys::editor::{IDimension, IEditorMinimapOptions}, }; use wasm_bindgen::{closure::Closure, JsCast}; -use crate::store::{editor::EditorState, query::QueryState}; +use crate::{ + modals, + store::{editor::EditorStore, query::QueryStore}, +}; pub type ModelCell = Rc>>; -pub fn query_editor() -> impl IntoView { - let query_state = use_context::().unwrap(); - let run_query = create_action(move |query_store: &QueryState| { +pub fn component() -> impl IntoView { + let query_state = use_context::().unwrap(); + let run_query = create_action(move |query_store: &QueryStore| { let query_store = *query_store; async move { - query_store.run_query().await; + query_store.run_query().await.unwrap(); } }); - let editor = use_context::().unwrap().editor; + let editor = use_context::().unwrap().editor; let node_ref = create_node_ref(); let _ = use_event_listener(node_ref, ev::keydown, move |event| { if event.key() == "Enter" && event.ctrl_key() { @@ -54,7 +57,42 @@ pub fn query_editor() -> impl IntoView { }); }); + let show = create_rw_signal(false); + let _ = use_event_listener(use_document(), ev::keydown, move |event| { + if event.key() == "Escape" { + show.set(false); + } + }); + let query_store = use_context::().unwrap(); + let run_query = create_action(move |query_store: &QueryStore| { + let query_store = *query_store; + async move { query_store.run_query().await } + }); + div() - .classes("border-b-1 border-neutral-200 sticky") + .classes("relative border-b-1 border-neutral-200 sticky") .node_ref(node_ref) + .child(div().child(modals::custom_query::component(show))) + .child( + div() + .classes( + "absolute bottom-0 items-center flex justify-end px-4 left-0 w-full h-10 bg-gray-50", + ) + .child( + div() + .classes("flex flex-row gap-2 text-xs") + .child( + button() + .classes("p-1 border-1 border-neutral-200 bg-white hover:bg-neutral-200 rounded-md") + .on(ev::click, move |_| show.set(true)) + .child("Save Query"), + ) + .child( + button() + .classes("p-1 border-1 border-neutral-200 bg-white hover:bg-neutral-200 rounded-md") + .on(ev::click, move |_| run_query.dispatch(query_store)) + .child("Query"), + ), + ), + ) } diff --git a/src/query_table.rs b/src/query_table.rs index 828820f..9042aa7 100644 --- a/src/query_table.rs +++ b/src/query_table.rs @@ -1,16 +1,14 @@ -use crate::{ - enums::QueryTableLayout, grid_view::grid_view, record_view::record_view, store::query::QueryState, -}; +use crate::{enums::QueryTableLayout, grid_view, record_view, store::query::QueryStore}; use leptos::{html::*, *}; -pub fn query_table() -> impl IntoView { - let query_state = use_context::().unwrap(); +pub fn component() -> impl IntoView { + let query_state = use_context::().unwrap(); let table_view = use_context::>().unwrap(); let table_layout = move || match query_state.sql_result.get() { None => div(), Some(_) => match table_view() { - QueryTableLayout::Grid => div().child(grid_view()), - QueryTableLayout::Records => div().child(record_view()), + QueryTableLayout::Grid => div().child(grid_view::component()), + QueryTableLayout::Records => div().child(record_view::component()), }, }; let when = move || !query_state.is_loading.get(); diff --git a/src/record_view.rs b/src/record_view.rs index 83fe468..f0085d6 100644 --- a/src/record_view.rs +++ b/src/record_view.rs @@ -1,9 +1,9 @@ -use leptos::{html::*, leptos_dom::Each, *}; +use leptos::{html::*, *}; -use crate::store::query::QueryState; +use crate::store::query::QueryStore; -pub fn record_view() -> impl IntoView { - let query_state = use_context::().unwrap(); +pub fn component() -> impl IntoView { + let query_state = use_context::().unwrap(); let columns = query_state.sql_result.get().unwrap().0.clone(); let first_row = query_state .sql_result @@ -13,10 +13,7 @@ pub fn record_view() -> impl IntoView { .first() .unwrap() .clone(); - let columns_with_values = columns - .into_iter() - .zip(first_row.into_iter()) - .collect::>(); + let columns_with_values = columns.into_iter().zip(first_row).collect::>(); // 2 columns table Properties, Values table() @@ -29,10 +26,10 @@ pub fn record_view() -> impl IntoView { .child(th().classes("text-xs px-4").child("Values")), ), ) - .child(tbody().child(Each::new( - move || columns_with_values.clone(), - move |n| n.clone(), - move |(col, val)| { + .child(tbody().child(For(ForProps { + each: move || columns_with_values.clone(), + key: |(col, _)| col.clone(), + children: move |(col, val)| { tr() .classes("divide-y divide-gray-200") .child( @@ -42,5 +39,5 @@ pub fn record_view() -> impl IntoView { ) .child(td().classes("px-4 text-xs hover:bg-gray-100").child(val)) }, - ))) + }))) } diff --git a/src/sidebar.rs b/src/sidebar.rs deleted file mode 100644 index 29770b5..0000000 --- a/src/sidebar.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::{ - invoke::{Invoke, InvokeProjectsArgs}, - queries::queries, - store::db::DBStore, - tables::tables, - wasm_functions::invoke, -}; -use leptos::{html::*, *}; - -pub fn sidebar() -> impl IntoView { - let db_state = use_context::().unwrap(); - let select_project_details = create_action(move |(db, project): &(DBStore, String)| { - let db_clone = *db; - let project = project.clone(); - async move { db_clone.select_project_details(project).await } - }); - let projects = create_resource( - move || db_state.is_connecting.get(), - move |_| async move { - let projects = invoke( - &Invoke::select_projects.to_string(), - serde_wasm_bindgen::to_value(&InvokeProjectsArgs).unwrap_or_default(), - ) - .await; - serde_wasm_bindgen::from_value::>(projects).unwrap() - }, - ); - let delete_project = create_action(move |(db, project): &(DBStore, String)| { - let mut db_clone = *db; - let project = project.clone(); - async move { db_clone.delete_project(project).await } - }); - let projects_result = move || { - projects - .get() - .unwrap_or_default() - .into_iter() - .enumerate() - .map(|(idx, project)| { - div() - .prop("key", idx) - .classes("flex flex-row justify-between items-center pl-1") - .child( - button() - .classes("hover:font-semibold text-sm") - .child(&project) - .on(ev::click, { - let project = project.clone(); - move |_| select_project_details.dispatch((db_state, project.clone())) - }), - ) - .child( - button() - .classes("px-2 rounded-full hover:bg-gray-200") - .child("-") - .on(ev::click, { - let project = project.clone(); - move |_| { - delete_project.dispatch((db_state, project.clone())); - } - }), - ) - }) - .collect_view() - }; - - div() - .classes("flex border-r-1 min-w-[320px] justify-between border-neutral-200 flex-col p-4") - .child( - div() - .classes("flex flex-col overflow-auto") - .child( - div() - .child( - div() - .classes("flex flex-row justify-between items-center") - .child(p().classes("font-semibold text-lg").child("Projects")) - .child( - button() - .classes("px-2 rounded-full hover:bg-gray-200") - .child("+") - .on(ev::click, move |_| db_state.reset()), - ), - ) - .child(projects_result.into_view()), - ) - .child( - div() - .classes("flex flex-col overflow-y-auto h-[calc(100vh-200px)] mt-4") - .child( - div().classes("font-semibold sticky top-0 bg-white").child( - div() - .classes("flex flex-row justify-between gap-2 items-center font-semibold text-lg") - .child("Connected to: ") - .child(move || db_state.project.get()), - ), - ) - .child(Show(ShowProps { - when: move || db_state.is_connecting.get(), - children: ChildrenFn::to_children(move || { - Fragment::new(vec![p().child("Loading...").into_view()]) - }), - fallback: ViewFn::from(div), - })) - .child(move || { - db_state - .schemas - .get() - .into_iter() - .map(|(schema, toggle)| { - let s = schema.clone(); - div() - .classes("pl-1 text-xs") - .prop("key", &schema) - .child( - button() - .classes(if toggle { - "font-semibold" - } else { - "hover:font-semibold" - }) - .classes("text-sm") - .on(ev::click, move |_| { - let s_clone = s.clone(); - db_state.schemas.update(move |prev| { - prev.insert(s_clone, !toggle); - }); - }) - .child(&schema), - ) - .child(div().classes("pl-1").child(Show(ShowProps { - when: move || toggle, - children: ChildrenFn::to_children(move || { - Fragment::new(vec![tables(schema.clone()).into_view()]) - }), - fallback: ViewFn::from(div), - }))) - }) - .collect_view() - }), - ), - ) - .child( - div() - .classes("py-2") - .child(p().classes("font-semibold text-lg").child("Saved Queries")) - .child(div().classes("text-sm").child(queries().into_view())), - ) -} diff --git a/src/sidebar/index.rs b/src/sidebar/index.rs new file mode 100644 index 0000000..2a7b3a0 --- /dev/null +++ b/src/sidebar/index.rs @@ -0,0 +1,62 @@ +use common::project::ProjectDetails; +use leptos::{html::*, IntoView, *}; +use leptos_use::{use_document, use_event_listener}; + +use crate::{ + invoke::{Invoke, InvokeSelectProjectsArgs}, + modals, + store::projects::ProjectsStore, + wasm_functions::invoke, +}; + +use super::{project, queries}; + +pub fn component() -> impl IntoView { + let projects_state = 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 projects = create_resource( + move || projects_state.0.get(), + move |_| async move { + let args = serde_wasm_bindgen::to_value(&InvokeSelectProjectsArgs).unwrap_or_default(); + let projects = invoke(&Invoke::select_projects.to_string(), args).await; + let projects = serde_wasm_bindgen::from_value::>(projects).unwrap(); + projects_state.set_projects(projects).unwrap() + }, + ); + + div() + .classes("flex border-r-1 min-w-[320px] justify-between border-neutral-200 flex-col p-4") + .child(modals::connection::component(show)) + .child( + div().classes("flex flex-col overflow-auto").child( + div() + .child( + div() + .classes("flex flex-row justify-between items-center") + .child(p().classes("font-semibold text-lg").child("Projects")) + .child( + button() + .classes("px-2 rounded-full hover:bg-gray-200") + .child("+") + .on(ev::click, move |_| show.set(true)), + ), + ) + .child(For(ForProps { + each: move || projects.get().unwrap_or_default(), + key: |(project, _)| project.clone(), + children: |(project, _)| project::component(project), + })), + ), + ) + .child( + div() + .classes("py-2") + .child(p().classes("font-semibold text-lg").child("Saved Queries")) + .child(div().classes("text-sm").child(queries::component())), + ) +} diff --git a/src/sidebar/mod.rs b/src/sidebar/mod.rs new file mode 100644 index 0000000..35127ed --- /dev/null +++ b/src/sidebar/mod.rs @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..c01025a --- /dev/null +++ b/src/sidebar/project.rs @@ -0,0 +1,66 @@ +use leptos::{html::*, *}; + +use crate::store::{active_project::ActiveProjectStore, projects::ProjectsStore}; + +use super::schemas; + +pub fn component(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(); + } + }); + + div() + .classes("pl-1 text-xs") + .child( + div() + .classes("flex flex-row justify-between items-center") + .child( + button() + .classes("hover:font-semibold") + .child(&project) + .on(ev::click, { + let project = project.clone(); + move |_| { + active_project_store.0.set(Some(project.clone())); + set_show_schemas(!show_schemas()); + } + }), + ) + .child( + button() + .classes("px-2 rounded-full hover:bg-gray-200") + .child("-") + .on(ev::click, { + let project = project.clone(); + move |_| { + delete_project.dispatch((projects_store, project.clone())); + } + }), + ), + ) + .child(div().classes("pl-1").child(Suspense(SuspenseProps { + fallback: ViewFn::from(|| "Loading..."), + children: ChildrenFn::to_children(move || { + let project = project.clone(); + Fragment::new(vec![Show(ShowProps { + children: { + let project = project.clone(); + ChildrenFn::to_children(move || { + let project = project.clone(); + Fragment::new(vec![schemas::component(project.clone()).into_view()]) + }) + }, + when: show_schemas, + fallback: ViewFn::default(), + }) + .into_view()]) + }), + }))) +} diff --git a/src/sidebar/queries.rs b/src/sidebar/queries.rs new file mode 100644 index 0000000..710423a --- /dev/null +++ b/src/sidebar/queries.rs @@ -0,0 +1,18 @@ +use crate::store::query::QueryStore; +use leptos::*; + +use super::query; + +pub fn component() -> impl IntoView { + let query_state = use_context::().unwrap(); + let queries = create_resource( + move || query_state.saved_queries.get(), + move |_| async move { query_state.select_queries().await.unwrap() }, + ); + + For(ForProps { + each: move || queries.get().unwrap_or_default(), + key: |(key, _)| key.clone(), + children: move |(key, _)| query::component(key), + }) +} diff --git a/src/sidebar/query.rs b/src/sidebar/query.rs new file mode 100644 index 0000000..993624e --- /dev/null +++ b/src/sidebar/query.rs @@ -0,0 +1,53 @@ +use leptos::{html::*, *}; +use leptos_icons::*; + +use crate::store::query::QueryStore; + +pub fn component(key: String) -> impl IntoView { + let query_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::>() + }); + + div() + .classes("flex flex-row justify-between items-center") + .child( + button() + .classes("hover:font-semibold") + .child( + div() + .classes("flex flex-row items-center gap-1") + .child(Icon(IconProps { + icon: MaybeSignal::derive(|| Icon::from(HiIcon::HiCircleStackOutlineLg)), + width: Some(MaybeSignal::derive(|| String::from("12"))), + height: Some(MaybeSignal::derive(|| String::from("12"))), + class: None, + style: None, + })) + .child(splitted_key.clone().get()[1].clone()), + ) + .on(ev::click, { + let key = key.clone(); + move |_| { + query_store.load_query(&key).unwrap(); + } + }), + ) + .child( + button() + .classes("px-2 rounded-full hover:bg-gray-200") + .child("-") + .on(ev::click, move |_| { + let key = key.clone(); + spawn_local(async move { + query_store.delete_query(&key).await.unwrap(); + }) + }), + ) +} diff --git a/src/sidebar/schema.rs b/src/sidebar/schema.rs new file mode 100644 index 0000000..0c3f591 --- /dev/null +++ b/src/sidebar/schema.rs @@ -0,0 +1,36 @@ +use leptos::{html::*, *}; + +use super::tables; + +pub fn component(schema: String, project: String) -> impl IntoView { + let (show_tables, set_show_tables) = create_signal(false); + + div() + .child( + div() + .classes("hover:font-semibold cursor-pointer sticky top-0 z-10 bg-white") + .child(&schema) + .on(ev::click, move |_| set_show_tables(!show_tables())), + ) + .child(div().classes("pl-1").child(Suspense(SuspenseProps { + fallback: ViewFn::from(|| "Loading..."), + children: ChildrenFn::to_children(move || { + let schema = schema.clone(); + let project = project.clone(); + Fragment::new(vec![Show(ShowProps { + children: { + let schema = schema.clone(); + let project = project.clone(); + ChildrenFn::to_children(move || { + let schema = schema.clone(); + let project = project.clone(); + Fragment::new(vec![tables::component(schema, project).into_view()]) + }) + }, + when: show_tables, + fallback: ViewFn::default(), + }) + .into_view()]) + }), + }))) +} diff --git a/src/sidebar/schemas.rs b/src/sidebar/schemas.rs new file mode 100644 index 0000000..a908bcf --- /dev/null +++ b/src/sidebar/schemas.rs @@ -0,0 +1,25 @@ +use leptos::*; + +use super::schema; +use crate::store::projects::ProjectsStore; + +pub fn component(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() } + }, + ); + + For(ForProps { + each: move || schemas.get().unwrap_or_default(), + key: |schema| schema.clone(), + children: move |s| { + let project = project_clone.clone(); + schema::component(s, project) + }, + }) +} diff --git a/src/sidebar/table.rs b/src/sidebar/table.rs new file mode 100644 index 0000000..408e071 --- /dev/null +++ b/src/sidebar/table.rs @@ -0,0 +1,38 @@ +use leptos::{html::*, *}; +use leptos_icons::*; + +use crate::store::{active_project::ActiveProjectStore, editor::EditorStore, query::QueryStore}; + +pub fn component(table: (String, String), project: String, schema: String) -> impl IntoView { + let query_store = use_context::().unwrap(); + let editor_store = use_context::().unwrap(); + let active_project = use_context::().unwrap(); + let query = create_action(move |(schema, table): &(String, String)| { + let project = project.clone(); + let schema = schema.clone(); + let table = table.clone(); + active_project.0.set(Some(project.clone())); + editor_store.set_value(&format!("SELECT * FROM {}.{} LIMIT 100;", schema, table)); + async move { query_store.run_query().await.unwrap() } + }); + + div() + .classes("flex flex-row justify-between items-center hover:font-semibold cursor-pointer") + .child( + div() + .classes("flex flex-row items-center gap-1") + .child(Icon(IconProps { + icon: MaybeSignal::derive(|| Icon::from(HiIcon::HiTableCellsOutlineLg)), + width: Some(MaybeSignal::derive(|| String::from("12"))), + height: Some(MaybeSignal::derive(|| String::from("12"))), + class: None, + style: None, + })) + .child(p().child(table.clone().0)), + ) + .child(p().child(table.clone().1)) + .on(ev::click, { + let table = table.clone(); + move |_| query.dispatch((schema.clone(), table.0.clone())) + }) +} diff --git a/src/sidebar/tables.rs b/src/sidebar/tables.rs new file mode 100644 index 0000000..c1b083f --- /dev/null +++ b/src/sidebar/tables.rs @@ -0,0 +1,29 @@ +use leptos::{html::*, *}; + +use super::table; +use crate::store::projects::ProjectsStore; + +pub fn component(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() + } + }, + ); + + div().child(For(ForProps { + each: move || tables.get().unwrap_or_default(), + key: |table| table.0.clone(), + children: move |t| table::component(t, project.clone(), schema.clone()), + })) +} diff --git a/src/store/active_project.rs b/src/store/active_project.rs new file mode 100644 index 0000000..7ac4113 --- /dev/null +++ b/src/store/active_project.rs @@ -0,0 +1,16 @@ +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 { + pub fn new() -> Self { + Self(create_rw_signal(None)) + } +} diff --git a/src/store/db.rs b/src/store/db.rs deleted file mode 100644 index e871e8c..0000000 --- a/src/store/db.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::{ - invoke::{Invoke, InvokeDeleteProjectArgs, InvokePostgresConnectionArgs, InvokeTablesArgs}, - wasm_functions::invoke, -}; -use leptos::{create_rw_signal, RwSignal, SignalGetUntracked, SignalSet, SignalUpdate}; -use std::collections::BTreeMap; - -#[derive(Clone, Copy, Debug)] -pub struct DBStore { - pub project: RwSignal, - pub db_host: RwSignal, - pub db_port: RwSignal, - pub db_user: RwSignal, - pub db_password: RwSignal, - pub schemas: RwSignal>, - pub is_connecting: RwSignal, - #[allow(clippy::type_complexity)] - pub tables: RwSignal>>, -} - -impl Default for DBStore { - fn default() -> Self { - Self::new(None, None, None, None, None) - } -} - -impl DBStore { - pub fn new( - project: Option, - db_host: Option, - db_post: Option, - db_user: Option, - db_password: Option, - ) -> Self { - Self { - project: create_rw_signal(project.unwrap_or_default()), - db_host: create_rw_signal(db_host.unwrap_or_default()), - db_port: create_rw_signal(db_post.unwrap_or_default()), - db_user: create_rw_signal(db_user.unwrap_or_default()), - db_password: create_rw_signal(db_password.unwrap_or_default()), - schemas: create_rw_signal(BTreeMap::new()), - is_connecting: create_rw_signal(false), - tables: create_rw_signal(BTreeMap::new()), - } - } - - pub fn reset(&self) { - self.project.set(String::new()); - self.db_host.set(String::new()); - self.db_port.set(String::new()); - self.db_user.set(String::new()); - self.db_password.set(String::new()); - self.is_connecting.set(false); - } - - pub fn create_connection_string(&self) -> String { - format!( - "user={} password={} host={} port={}", - self.db_user.get_untracked(), - self.db_password.get_untracked(), - self.db_host.get_untracked(), - self.db_port.get_untracked(), - ) - } - - pub async fn connect(&self) { - self.is_connecting.set(true); - let args = serde_wasm_bindgen::to_value(&InvokePostgresConnectionArgs { - project: self.project.get_untracked(), - key: self.create_connection_string(), - }) - .unwrap(); - let schemas = invoke(&Invoke::pg_connector.to_string(), args).await; - let schemas = serde_wasm_bindgen::from_value::>(schemas).unwrap(); - for schema in schemas { - self.schemas.update(|prev| { - prev.insert(schema.clone(), false); - }); - } - self.is_connecting.set(false); - } - - pub async fn select_tables(&self, schema: String) -> Result, ()> { - if let Some(tables) = self.tables.get_untracked().get(&schema) { - if !tables.is_empty() { - return Ok(tables.clone()); - } - } - - let args = serde_wasm_bindgen::to_value(&InvokeTablesArgs { - schema: schema.to_string(), - }) - .unwrap(); - let tables = invoke(&Invoke::select_schema_tables.to_string(), args).await; - let tables = serde_wasm_bindgen::from_value::>(tables).unwrap(); - let tables = tables - .into_iter() - .map(|(t, size)| (t, size, false)) - .collect::>(); - self.tables.update(|prev| { - prev.insert(schema, tables.clone()); - }); - Ok(tables) - } - - pub async fn select_project_details(&self, project: String) -> Result<(), ()> { - let args = serde_wasm_bindgen::to_value(&InvokePostgresConnectionArgs { - project: project.clone(), - key: String::new(), - }) - .unwrap(); - let project_details = invoke(&Invoke::select_project_details.to_string(), args).await; - let project_details = - serde_wasm_bindgen::from_value::>(project_details).unwrap(); - self.project.set(project); - self - .db_user - .set(project_details.get("user").unwrap().to_string()); - self - .db_password - .set(project_details.get("password").unwrap().to_string()); - self - .db_host - .set(project_details.get("host").unwrap().to_string()); - self - .db_port - .set(project_details.get("port").unwrap().to_string()); - self.connect().await; - Ok(()) - } - - pub async fn delete_project(&mut self, project: String) -> Result<(), ()> { - let args = serde_wasm_bindgen::to_value(&InvokeDeleteProjectArgs { project }).unwrap(); - invoke(&Invoke::delete_project.to_string(), args).await; - Ok(()) - } -} diff --git a/src/store/editor.rs b/src/store/editor.rs index fde63de..01f1814 100644 --- a/src/store/editor.rs +++ b/src/store/editor.rs @@ -3,17 +3,17 @@ use leptos::{create_rw_signal, RwSignal, SignalGetUntracked}; use crate::query_editor::ModelCell; #[derive(Copy, Clone, Debug)] -pub struct EditorState { +pub struct EditorStore { pub editor: RwSignal, } -impl Default for EditorState { +impl Default for EditorStore { fn default() -> Self { Self::new() } } -impl EditorState { +impl EditorStore { pub fn new() -> Self { Self { editor: create_rw_signal(ModelCell::default()), diff --git a/src/store/mod.rs b/src/store/mod.rs index a53a144..78a0154 100644 --- a/src/store/mod.rs +++ b/src/store/mod.rs @@ -1,3 +1,4 @@ -pub mod db; +pub mod active_project; pub mod editor; +pub mod projects; pub mod query; diff --git a/src/store/projects.rs b/src/store/projects.rs new file mode 100644 index 0000000..009de1e --- /dev/null +++ b/src/store/projects.rs @@ -0,0 +1,173 @@ +use std::{borrow::Borrow, collections::BTreeMap}; + +use common::project::ProjectDetails; +use leptos::{ + create_rw_signal, error::Result, RwSignal, SignalGet, SignalGetUntracked, SignalUpdate, +}; +use serde::{Deserialize, Serialize}; + +use crate::{ + enums::ProjectConnectionStatus, + invoke::{Invoke, InvokeDeleteProjectArgs, InvokePostgresConnectionArgs, InvokeSchemaTablesArgs}, + wasm_functions::invoke, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Project { + pub host: String, + pub port: String, + pub user: String, + pub password: String, + pub schemas: Vec, + pub tables: BTreeMap>, + pub status: ProjectConnectionStatus, +} + +#[derive(Clone, Copy, Debug)] +pub struct ProjectsStore(pub RwSignal>); + +impl Default for ProjectsStore { + fn default() -> Self { + Self::new() + } +} + +impl ProjectsStore { + pub fn new() -> Self { + Self(create_rw_signal(BTreeMap::default())) + } + + pub fn set_projects(&self, projects: Vec) -> Result> { + let projects = projects + .into_iter() + .map(|project| { + ( + project.name, + Project { + host: project.host, + port: project.port, + user: project.user, + password: project.password, + schemas: Vec::new(), + tables: BTreeMap::new(), + status: ProjectConnectionStatus::default(), + }, + ) + }) + .collect::>(); + self.0.update(|prev| { + *prev = projects; + }); + Ok(self.0.get_untracked().clone()) + } + + pub fn insert_project(&self, project: ProjectDetails) -> Result<()> { + self.0.update(|prev| { + prev.insert( + project.name.clone(), + Project { + host: project.host, + port: project.port, + user: project.user, + password: project.password, + schemas: Vec::new(), + tables: BTreeMap::new(), + status: ProjectConnectionStatus::default(), + }, + ); + }); + 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_key: &str) -> String { + let projects = self.0.get_untracked(); + let (_, project) = projects.get_key_value(project_key).unwrap(); + + format!( + "user={} password={} host={} port={}", + project.user, project.password, project.host, project.port, + ) + } + + pub async fn connect(&self, project: &str) -> Result> { + let projects = self.0; + + if let Some(project) = projects.get_untracked().get(project) { + if !project.schemas.is_empty() { + return Ok(project.schemas.clone()); + } + } + + let connection_string = self.create_project_connection_string(project); + let args = serde_wasm_bindgen::to_value(&InvokePostgresConnectionArgs { + project: project.to_string(), + key: connection_string, + }) + .unwrap(); + let schemas = invoke(&Invoke::pg_connector.to_string(), args).await; + let mut schemas = serde_wasm_bindgen::from_value::>(schemas).unwrap(); + schemas.sort(); + projects.update(|prev| { + let project = prev.get_mut(project).unwrap(); + project.schemas = schemas; + project.status = ProjectConnectionStatus::Connected; + }); + let schemas = self.0.get_untracked().get(project).unwrap().schemas.clone(); + Ok(schemas) + } + + pub async fn retrieve_tables( + &self, + project: &str, + schema: &str, + ) -> Result> { + let projects = self.0; + let p = projects.borrow().get_untracked(); + let p = p.get(project).unwrap(); + if let Some(tables) = p.tables.get(schema) { + if !tables.is_empty() { + return Ok(tables.clone()); + } + } + let args = serde_wasm_bindgen::to_value(&InvokeSchemaTablesArgs { + project: project.to_string(), + schema: schema.to_string(), + }) + .unwrap(); + let tables = invoke(&Invoke::select_schema_tables.to_string(), args).await; + let tables = serde_wasm_bindgen::from_value::>(tables).unwrap(); + projects.update(|prev| { + let project = prev.get_mut(project).unwrap(); + project.tables.insert(schema.to_string(), tables.clone()); + }); + let tables = self + .0 + .get_untracked() + .get(project) + .unwrap() + .tables + .get(schema) + .unwrap() + .clone(); + Ok(tables) + } + + pub async fn delete_project(&self, project: &str) -> Result<()> { + let args = serde_wasm_bindgen::to_value(&InvokeDeleteProjectArgs { + project: project.to_string(), + }) + .unwrap(); + invoke(&Invoke::delete_project.to_string(), args).await; + let projects = self.0; + projects.update(|prev| { + prev.remove(project); + }); + Ok(()) + } +} diff --git a/src/store/query.rs b/src/store/query.rs index 13ff9f4..2d041bd 100644 --- a/src/store/query.rs +++ b/src/store/query.rs @@ -1,31 +1,32 @@ use std::collections::BTreeMap; -use leptos::*; +use leptos::{error::Result, *}; use crate::{ invoke::{ - Invoke, InvokeDeleteQueryArgs, InvokeInsertQueryArgs, InvokeQueryArgs, InvokeSelectQueriesArgs, + Invoke, InvokeDeleteQueryArgs, InvokeInsertQueryArgs, InvokeSelectQueriesArgs, + InvokeSqlResultArgs, }, wasm_functions::invoke, }; -use super::editor::EditorState; +use super::{active_project::ActiveProjectStore, editor::EditorStore, projects::ProjectsStore}; #[derive(Clone, Copy, Debug)] -pub struct QueryState { +pub struct QueryStore { #[allow(clippy::type_complexity)] pub sql_result: RwSignal, Vec>)>>, pub is_loading: RwSignal, pub saved_queries: RwSignal>, } -impl Default for QueryState { +impl Default for QueryStore { fn default() -> Self { Self::new() } } -impl QueryState { +impl QueryStore { pub fn new() -> Self { Self { sql_result: create_rw_signal(None), @@ -34,11 +35,22 @@ impl QueryState { } } - pub async fn run_query(&self) { + pub async fn run_query(&self) -> Result<()> { + let active_project = use_context::().unwrap(); + let active_project = active_project.0.get_untracked().unwrap(); + let projects_store = use_context::().unwrap(); + if projects_store + .0 + .get_untracked() + .get(&active_project) + .is_none() + { + projects_store.connect(&active_project).await?; + } self.is_loading.update(|prev| { *prev = true; }); - let editor_state = use_context::().unwrap(); + let editor_state = use_context::().unwrap(); let position: monaco::sys::Position = editor_state .editor .get_untracked() @@ -49,12 +61,12 @@ impl QueryState { .get_position() .unwrap(); let sql = editor_state.get_value(); - let sql = match self.find_query_for_line(&sql, position.line_number()) { - Some(query) => Some(query), - None => None, - }; - let args = serde_wasm_bindgen::to_value(&InvokeQueryArgs { - sql: sql.unwrap().query, + let sql = self + .find_query_for_line(&sql, position.line_number()) + .unwrap(); + let args = serde_wasm_bindgen::to_value(&InvokeSqlResultArgs { + project: active_project, + sql: sql.query, }) .unwrap(); let data = invoke(&Invoke::select_sql_result.to_string(), args).await; @@ -63,9 +75,10 @@ impl QueryState { self.is_loading.update(|prev| { *prev = false; }); + Ok(()) } - pub async fn select_queries(&self) -> Result<(), ()> { + pub async fn select_queries(&self) -> Result> { let args = serde_wasm_bindgen::to_value(&InvokeSelectQueriesArgs).unwrap_or_default(); let saved_queries = invoke(&Invoke::select_queries.to_string(), args).await; let queries = @@ -73,14 +86,14 @@ impl QueryState { self.saved_queries.update(|prev| { *prev = queries.into_iter().collect(); }); - Ok(()) + Ok(self.saved_queries.get_untracked().clone()) } - pub async fn insert_query(&self, key: &str) -> Result<(), ()> { - let editor_state = use_context::().unwrap(); + pub async fn insert_query(&self, key: &str, project: &str) -> Result<()> { + let editor_state = use_context::().unwrap(); let sql = editor_state.get_value(); let args = serde_wasm_bindgen::to_value(&InvokeInsertQueryArgs { - key: key.to_string(), + key: format!("{}:{}", project, key), sql, }); invoke(&Invoke::insert_query.to_string(), args.unwrap_or_default()).await; @@ -88,7 +101,7 @@ impl QueryState { Ok(()) } - pub async fn delete_query(&self, key: &str) -> Result<(), ()> { + pub async fn delete_query(&self, key: &str) -> Result<()> { let args = serde_wasm_bindgen::to_value(&InvokeDeleteQueryArgs { key: key.to_string(), }); @@ -97,10 +110,14 @@ impl QueryState { Ok(()) } - pub fn load_query(&self, key: &str) { + 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 = self.saved_queries.get_untracked().get(key).unwrap().clone(); - let editor_state = use_context::().unwrap(); + let editor_state = use_context::().unwrap(); editor_state.set_value(&query); + Ok(()) } // TODO: improve this diff --git a/src/tables.rs b/src/tables.rs deleted file mode 100644 index df34602..0000000 --- a/src/tables.rs +++ /dev/null @@ -1,90 +0,0 @@ -use crate::store::{db::DBStore, editor::EditorState, query::QueryState}; -use leptos::{html::*, *}; -use leptos_icons::*; - -pub fn tables(schema: String) -> impl IntoView { - let db = use_context::().unwrap(); - let query_state = use_context::().unwrap(); - let tables = create_resource( - || {}, - move |_| { - let schema = schema.clone(); - async move { db.select_tables(schema).await.unwrap() } - }, - ); - let when = move || !tables.get().unwrap_or_default().is_empty(); - let show_fallback = move || ViewFn::from(|| "No tables found"); - let fallback = ViewFn::from(|| "Loading..."); - let tables = move || { - div().child( - tables - .get() - .unwrap_or_default() - .into_iter() - .enumerate() - .map(|(i, (table, size, is_selected))| { - let table_clone = table.clone(); - div() - .prop("key", i) - .classes(if is_selected { - "font-semibold cursor-pointer" - } else { - "hover:font-semibold cursor-pointer" - }) - .on(ev::click, move |_| { - let schema = db - .schemas - .get_untracked() - .iter() - .find(|(_, is_selected)| **is_selected) - .unwrap() - .0 - .clone(); - let t_clone = table_clone.clone(); - spawn_local(async move { - let editor_state = use_context::().unwrap(); - editor_state.set_value(&format!("SELECT * FROM {}.{} LIMIT 100;", schema, t_clone)); - query_state.run_query().await; - }); - let table_clone = table_clone.clone(); - tables.update(move |prev| { - prev.iter_mut().for_each(|tables| { - tables.iter_mut().for_each(|(t, _, s)| { - *s = t == &table_clone; - }); - }); - }); - }) - .child( - div() - .classes("flex flex-row justify-between items-center") - .child( - div() - .classes("flex flex-row items-center gap-1") - .child(Icon(IconProps { - icon: MaybeSignal::derive(|| Icon::from(HiIcon::HiTableCellsOutlineLg)), - width: Some(MaybeSignal::derive(|| String::from("12"))), - height: Some(MaybeSignal::derive(|| String::from("12"))), - class: None, - style: None, - })) - .child(p().child(table)), - ) - .child(p().child(size)), - ) - }) - .collect_view(), - ) - }; - let show_children = - move || ChildrenFn::to_children(move || Fragment::new(vec![tables().into_view()])); - let children = ChildrenFn::to_children(move || { - Fragment::new(vec![Show(ShowProps { - children: show_children(), - when, - fallback: show_fallback(), - }) - .into_view()]) - }); - Suspense::(SuspenseProps { fallback, children }) -}