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