From c8c10692deeb2dd47f871d44a0f24c05652604c7 Mon Sep 17 00:00:00 2001 From: Daniel Boros Date: Sat, 9 Dec 2023 03:01:21 +0100 Subject: [PATCH] feat: upgrade sidebar --- Cargo.toml | 4 +- leptosfmt.toml | 5 - rustfmt.toml | 4 +- src-tauri/Cargo.toml | 2 +- src/db_connector.rs | 29 +++--- src/invoke.rs | 39 +++++++- src/sidebar.rs | 223 +++++++++++++++++++++++------------------ src/store/db.rs | 229 ++++++++++++++++++++++--------------------- src/store/query.rs | 87 ++++++++-------- style/output.css | 25 +++++ 10 files changed, 371 insertions(+), 276 deletions(-) delete mode 100644 leptosfmt.toml diff --git a/Cargo.toml b/Cargo.toml index 9a7b0d7..0ad223a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-sql-gui-ui" -version = "0.0.0" +version = "1.0.0-alpha.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -18,5 +18,3 @@ monaco = "0.4.0" [workspace] members = ["src-tauri"] - - diff --git a/leptosfmt.toml b/leptosfmt.toml deleted file mode 100644 index a05eb83..0000000 --- a/leptosfmt.toml +++ /dev/null @@ -1,5 +0,0 @@ -max_width = 100 # Maximum width of each line -tab_spaces = 2 # Number of spaces per tab -indentation_style = "Spaces" # "Tabs", "Spaces" or "Auto" -newline_style = "Unix" # "Unix", "Windows" or "Auto" -attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve" \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml index 36c419b..a06d1b0 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,3 @@ -edition = "2021" \ No newline at end of file +edition = "2021" +max_width = 100 # Maximum width of each line +tab_spaces = 2 # Number of spaces per tab diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8ed80b9..b707504 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-sql-gui" -version = "0.0.0" +version = "1.0.0-alpha.1" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src/db_connector.rs b/src/db_connector.rs index 8d37c81..03d8765 100644 --- a/src/db_connector.rs +++ b/src/db_connector.rs @@ -3,22 +3,20 @@ use leptos::{html::*, *}; use crate::store::{db::DBStore, query::QueryState}; pub fn db_connector() -> impl IntoView { - let db = use_context::().unwrap(); - let refetch_projects = use_context::>>().unwrap(); - let connect = create_action(move |db: &DBStore| { - let mut db_clone = *db; - async move { - db_clone.connect().await; - refetch_projects.refetch(); - } - }); - 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 db = use_context::().unwrap(); + let connect = create_action(move |db: &DBStore| { + let mut 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 } + }); - header() + header() .attr( "class", "flex flex-row justify-between p-4 gap-2 border-b-1 border-neutral-200", @@ -108,4 +106,3 @@ pub fn db_connector() -> impl IntoView { ), ) } - diff --git a/src/invoke.rs b/src/invoke.rs index 3d96c9d..585d1d3 100644 --- a/src/invoke.rs +++ b/src/invoke.rs @@ -1,19 +1,44 @@ +use std::fmt::Display; + use serde::{Deserialize, Serialize}; +#[allow(non_camel_case_types)] +pub enum Invoke { + get_projects, + get_project_details, + remove_project, + get_sql_result, + get_schema_tables, + pg_connector, +} + +impl Display for Invoke { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Invoke::get_projects => write!(f, "get_projects"), + Invoke::get_project_details => write!(f, "get_project_details"), + Invoke::remove_project => write!(f, "remove_project"), + Invoke::get_sql_result => write!(f, "get_sql_result"), + Invoke::get_schema_tables => write!(f, "get_schema_tables"), + Invoke::pg_connector => write!(f, "pg_connector"), + } + } +} + #[derive(Serialize, Deserialize)] pub struct InvokePostgresConnectionArgs { - pub project: String, - pub key: String, + pub project: String, + pub key: String, } #[derive(Serialize, Deserialize)] pub struct InvokeTablesArgs { - pub schema: String, + pub schema: String, } #[derive(Serialize, Deserialize)] pub struct InvokeQueryArgs { - pub sql: String, + pub sql: String, } #[derive(Serialize, Deserialize)] @@ -21,6 +46,10 @@ pub struct InvokeProjectsArgs; #[derive(Serialize, Deserialize)] pub struct InvokeProjectDetailsArgs { - pub project: String, + pub project: String, } +#[derive(Serialize, Deserialize)] +pub struct InvokeRemoveProjectArgs { + pub project: String, +} diff --git a/src/sidebar.rs b/src/sidebar.rs index f68d106..2672378 100644 --- a/src/sidebar.rs +++ b/src/sidebar.rs @@ -1,102 +1,139 @@ use crate::{ - invoke::InvokeProjectsArgs, store::db::DBStore, tables::tables, wasm_functions::invoke, + invoke::{Invoke, InvokeProjectsArgs}, + store::db::DBStore, + tables::tables, + wasm_functions::invoke, }; use leptos::{html::*, *}; pub fn sidebar() -> impl IntoView { - let db = use_context::().unwrap(); - let get_project_details = create_action(move |(db, project): &(DBStore, String)| { - let mut db_clone = *db; - let project = project.clone(); - async move { db_clone.get_project_details(project).await } - }); - let projects = create_resource( - || {}, - move |_| async move { - let projects = invoke( - "get_projects", - serde_wasm_bindgen::to_value(&InvokeProjectsArgs).unwrap(), - ) - .await; - let projects = serde_wasm_bindgen::from_value::>(projects).unwrap(); - projects - }, - ); - provide_context(projects); + let mut db = use_context::().unwrap(); + let get_project_details = create_action(move |(db, project): &(DBStore, String)| { + let mut db_clone = *db; + let project = project.clone(); + async move { db_clone.get_project_details(project).await } + }); + let projects = create_resource( + move || db.is_connecting.get(), + move |_| async move { + let projects = invoke( + &Invoke::get_projects.to_string(), + serde_wasm_bindgen::to_value(&InvokeProjectsArgs).unwrap_or_default(), + ) + .await; + serde_wasm_bindgen::from_value::>(projects).unwrap() + }, + ); + let remove_project = create_action(move |db: &DBStore| { + let mut db_clone = *db; + async move { + db_clone.remove_project().await.unwrap(); + projects.refetch(); + } + }); + let projects_result = move || { + projects + .get() + .unwrap_or_default() + .into_iter() + .enumerate() + .map(|(idx, project)| { + div() + .attr("key", idx) + .attr("class", "flex flex-row justify-between items-center") + .child( + button() + .attr("class", "hover:font-semibold") + .child(&project) + .on(ev::click, { + let project = project.clone(); + move |_| get_project_details.dispatch((db, project.clone())) + }), + ) + .child( + button() + .attr("class", "px-2 rounded-full hover:bg-gray-200") + .child("-") + .on(ev::click, move |_| { + remove_project.dispatch(db); + projects.update(|prev| prev.as_mut().unwrap().retain(|p| p != &project.clone())); + }), + ) + }) + .collect_view() + }; - div() - .attr( - "class", - "flex border-r-1 border-neutral-200 flex-col gap-2 px-4 pt-4 overflow-auto", - ) - .child(Suspense(SuspenseProps { - children: ChildrenFn::to_children(move || { - Fragment::new(vec![div() - .child(p().attr("class", "font-semibold").child("Projects")) - .child(move || { - projects - .get() - .unwrap_or(Vec::new()) - .into_iter() - .enumerate() - .map(|(idx, project)| { - button() - .attr("key", idx) - .attr("class", "hover:font-semibold") - .child(&project) - .on(ev::click, move |_| { - get_project_details.dispatch((db.clone(), project.clone())) - }) - }) - .collect_view() - }) - .into_view()]) - }), - fallback: ViewFn::from(|| p().child("Loading...")), - })) - .child(p().attr("class", "font-semibold").child("Schemas")) - .child(Show(ShowProps { - when: move || db.is_connecting.get(), - children: ChildrenFn::to_children(move || { - Fragment::new(vec![p().child("Loading...").into_view()]) - }), - fallback: ViewFn::from(div), - })) - .child(move || { - db.schemas - .get() - .into_iter() - .map(|(schema, toggle)| { - let s = schema.clone(); - div() - .attr("key", &schema) - .child( - button() - .attr( - "class", - if toggle { - "font-semibold" - } else { - "hover:font-semibold" - }, - ) - .on(ev::click, move |_| { - let s_clone = s.clone(); - db.schemas.update(move |prev| { - prev.insert(s_clone, !toggle); - }); - }) - .child(&schema), - ) - .child(Show(ShowProps { - when: move || toggle, - children: ChildrenFn::to_children(move || { - Fragment::new(vec![tables(schema.clone()).into_view()]) - }), - fallback: ViewFn::from(div), - })) + div() + .attr( + "class", + "flex border-r-1 min-w-[200px] border-neutral-200 flex-col gap-2 px-4 pt-4 overflow-auto", + ) + .child( + div() + .attr("class", "flex w-full flex-row justify-between items-center") + .child(p().attr("class", "font-semibold").child("Projects")) + .child( + button() + .attr("class", "px-2 rounded-full hover:bg-gray-200") + .child("+") + .on(ev::click, move |_| db.reset()), + ), + ) + .child(Suspense(SuspenseProps { + children: ChildrenFn::to_children(move || { + Fragment::new(vec![Show(ShowProps { + when: move || !projects.get().unwrap_or_default().is_empty(), + fallback: ViewFn::from(|| p().attr("class", "text-xs").child("No projects")), + children: ChildrenFn::to_children(move || { + Fragment::new(vec![projects_result.into_view()]) + }), + }) + .into_view()]) + }), + fallback: ViewFn::from(|| p().child("Loading...")), + })) + .child(p().attr("class", "font-semibold").child("Schemas")) + .child(Show(ShowProps { + when: move || db.is_connecting.get(), + children: ChildrenFn::to_children(move || { + Fragment::new(vec![p().child("Loading...").into_view()]) + }), + fallback: ViewFn::from(div), + })) + .child(move || { + db.schemas + .get() + .into_iter() + .map(|(schema, toggle)| { + let s = schema.clone(); + div() + .attr("key", &schema) + .child( + button() + .attr( + "class", + if toggle { + "font-semibold" + } else { + "hover:font-semibold" + }, + ) + .on(ev::click, move |_| { + let s_clone = s.clone(); + db.schemas.update(move |prev| { + prev.insert(s_clone, !toggle); + }); }) - .collect_view() + .child(&schema), + ) + .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() + }) } - diff --git a/src/store/db.rs b/src/store/db.rs index 2005f03..b5c2e6d 100644 --- a/src/store/db.rs +++ b/src/store/db.rs @@ -1,133 +1,142 @@ use crate::{ - invoke::{InvokePostgresConnectionArgs, InvokeTablesArgs}, - wasm_functions::invoke, + invoke::{Invoke, InvokePostgresConnectionArgs, InvokeRemoveProjectArgs, InvokeTablesArgs}, + wasm_functions::invoke, }; use leptos::{create_rw_signal, RwSignal, SignalGetUntracked, SignalSet, SignalUpdate}; use std::collections::HashMap; #[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, - pub tables: RwSignal>>, + 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, + pub tables: RwSignal>>, } impl Default for DBStore { - fn default() -> Self { - Self::new(None, None, None, None, None) - } + 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(String::new())), - db_host: create_rw_signal(db_host.unwrap_or(String::new())), - db_port: create_rw_signal(db_post.unwrap_or(String::new())), - db_user: create_rw_signal(db_user.unwrap_or(String::new())), - db_password: create_rw_signal(db_password.unwrap_or(String::new())), - schemas: create_rw_signal(HashMap::new()), - is_connecting: create_rw_signal(false), - tables: create_rw_signal(HashMap::new()), - } + 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(HashMap::new()), + is_connecting: create_rw_signal(false), + tables: create_rw_signal(HashMap::new()), } + } - pub fn reset(&mut 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.schemas.set(HashMap::new()); - self.is_connecting.set(false); - self.tables.set(HashMap::new()); - } + pub fn reset(&mut 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 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(&mut self) { + if !self.schemas.get_untracked().is_empty() { + return; + } + 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 connect(&mut self) { - if !self.schemas.get_untracked().is_empty() { - return; - } - self.reset(); - 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("pg_connector", 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 get_tables(&mut self, schema: String) -> Result, ()> { + if let Some(tables) = self.tables.get_untracked().get(&schema) { + if !tables.is_empty() { + return Ok(tables.clone()); + } } - pub async fn get_tables(&mut 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::get_schema_tables.to_string(), args).await; + let mut tables = serde_wasm_bindgen::from_value::>(tables).unwrap(); + tables.sort(); + let tables = tables + .into_iter() + .map(|t| (t, false)) + .collect::>(); + self.tables.update(|prev| { + prev.insert(schema, tables.clone()); + }); + Ok(tables) + } - let args = serde_wasm_bindgen::to_value(&InvokeTablesArgs { - schema: schema.to_string(), - }) - .unwrap(); - let tables = invoke("get_schema_tables", args).await; - let mut tables = serde_wasm_bindgen::from_value::>(tables).unwrap(); - tables.sort(); - let tables = tables - .into_iter() - .map(|t| (t, false)) - .collect::>(); - self.tables.update(|prev| { - prev.insert(schema, tables.clone()); - }); - Ok(tables) - } + pub async fn get_project_details(&mut self, project: String) -> Result<(), ()> { + let args = serde_wasm_bindgen::to_value(&InvokePostgresConnectionArgs { + project: project.clone(), + key: String::new(), + }) + .unwrap(); + let project_details = invoke(&Invoke::get_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()); + Ok(()) + } - pub async fn get_project_details(&mut self, project: String) -> Result<(), ()> { - let args = serde_wasm_bindgen::to_value(&InvokePostgresConnectionArgs { - project: project.clone(), - key: String::new(), - }) - .unwrap(); - let project_details = invoke("get_project_details", 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()); - Ok(()) - } + pub async fn remove_project(&mut self) -> Result<(), ()> { + let args = serde_wasm_bindgen::to_value(&InvokeRemoveProjectArgs { + project: self.project.get_untracked(), + }) + .unwrap(); + invoke(&Invoke::remove_project.to_string(), args).await; + Ok(()) + } } - diff --git a/src/store/query.rs b/src/store/query.rs index 40374df..4e8739e 100644 --- a/src/store/query.rs +++ b/src/store/query.rs @@ -1,57 +1,60 @@ use leptos::{create_rw_signal, use_context, RwSignal, SignalGetUntracked, SignalUpdate}; -use crate::{invoke::InvokeQueryArgs, wasm_functions::invoke}; +use crate::{ + invoke::{Invoke, InvokeQueryArgs}, + wasm_functions::invoke, +}; use super::editor::EditorState; #[derive(Clone, Copy, Debug)] pub struct QueryState { - pub sql: RwSignal, - pub sql_result: RwSignal, Vec>)>>, - pub is_loading: RwSignal, + pub sql: RwSignal, + #[allow(clippy::type_complexity)] + pub sql_result: RwSignal, Vec>)>>, + pub is_loading: RwSignal, } impl Default for QueryState { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl QueryState { - pub fn new() -> Self { - Self { - sql: create_rw_signal(String::from("SELECT * FROM users LIMIT 100;")), - sql_result: create_rw_signal(Some((Vec::new(), Vec::new()))), - is_loading: create_rw_signal(false), - } - } - - pub async fn run_query(&self) { - self.is_loading.update(|prev| { - *prev = true; - }); - let editor = use_context::().unwrap().editor.get_untracked(); - let code = editor - .borrow() - .as_ref() - .unwrap() - .get_model() - .unwrap() - .get_value(); - - let args = serde_wasm_bindgen::to_value(&InvokeQueryArgs { - sql: code.to_string(), - }) - .unwrap(); - - let data = invoke("get_sql_result", args).await; - let data = serde_wasm_bindgen::from_value::<(Vec, Vec>)>(data).unwrap(); - self.sql_result.update(|prev| { - *prev = Some(data); - }); - self.is_loading.update(|prev| { - *prev = false; - }); + pub fn new() -> Self { + Self { + sql: create_rw_signal(String::from("SELECT * FROM users LIMIT 100;")), + sql_result: create_rw_signal(Some((Vec::new(), Vec::new()))), + is_loading: create_rw_signal(false), } + } + + pub async fn run_query(&self) { + self.is_loading.update(|prev| { + *prev = true; + }); + let editor = use_context::().unwrap().editor.get_untracked(); + let code = editor + .borrow() + .as_ref() + .unwrap() + .get_model() + .unwrap() + .get_value(); + + let args = serde_wasm_bindgen::to_value(&InvokeQueryArgs { + sql: code.to_string(), + }) + .unwrap(); + + let data = invoke(&Invoke::get_sql_result.to_string(), args).await; + let data = serde_wasm_bindgen::from_value::<(Vec, Vec>)>(data).unwrap(); + self.sql_result.update(|prev| { + *prev = Some(data); + }); + self.is_loading.update(|prev| { + *prev = false; + }); + } } - diff --git a/style/output.css b/style/output.css index 3d1e245..aefaccb 100644 --- a/style/output.css +++ b/style/output.css @@ -558,6 +558,10 @@ video { width: 100%; } +.min-w-\[200px\] { + min-width: 200px; +} + .flex-1 { flex: 1 1 0%; } @@ -578,6 +582,10 @@ video { flex-direction: column; } +.items-center { + align-items: center; +} + .justify-between { justify-content: space-between; } @@ -615,6 +623,10 @@ video { overflow-y: scroll; } +.rounded-full { + border-radius: 9999px; +} + .rounded-md { border-radius: 0.375rem; } @@ -650,6 +662,10 @@ video { padding: 0.25rem; } +.p-2 { + padding: 0.5rem; +} + .p-4 { padding: 1rem; } @@ -664,6 +680,11 @@ video { padding-bottom: 0.5rem; } +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + .pl-2 { padding-left: 0.5rem; } @@ -685,6 +706,10 @@ video { font-weight: 600; } +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + .hover\:bg-gray-100:hover { --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity));