├── .cargo └── config.toml ├── .github └── workflows │ ├── release.yml │ ├── rust-clippy.yml │ └── rust.yml ├── .gitignore ├── .taurignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── Cargo.toml ├── README.md ├── Trunk.toml ├── common ├── Cargo.toml └── src │ ├── enums.rs │ ├── lib.rs │ └── types │ ├── mod.rs │ └── pgsql.rs ├── index.html ├── input.css ├── leptosfmt.toml ├── proc-macros ├── Cargo.toml └── src │ └── lib.rs ├── public └── icon.png ├── rustfmt.toml ├── src-tauri ├── .gitignore ├── Cargo.toml ├── build.rs ├── capabilities │ └── migrated.json ├── gen │ └── schemas │ │ ├── acl-manifests.json │ │ ├── capabilities.json │ │ ├── desktop-schema.json │ │ └── macOS-schema.json ├── icons │ ├── 128x128.png │ ├── 128x128@2x.png │ ├── 32x32.png │ ├── Square107x107Logo.png │ ├── Square142x142Logo.png │ ├── Square150x150Logo.png │ ├── Square284x284Logo.png │ ├── Square30x30Logo.png │ ├── Square310x310Logo.png │ ├── Square44x44Logo.png │ ├── Square71x71Logo.png │ ├── Square89x89Logo.png │ ├── StoreLogo.png │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── src │ ├── dbs │ │ ├── mod.rs │ │ ├── project.rs │ │ └── query.rs │ ├── drivers │ │ ├── mod.rs │ │ └── pgsql.rs │ ├── main.rs │ └── utils.rs └── tauri.conf.json ├── src ├── app.rs ├── dashboard │ ├── index.rs │ ├── mod.rs │ ├── query_editor.rs │ └── query_table.rs ├── databases │ ├── mod.rs │ └── pgsql │ │ ├── driver.rs │ │ ├── index.rs │ │ ├── mod.rs │ │ ├── schema.rs │ │ └── table.rs ├── enums.rs ├── footer.rs ├── grid_view.rs ├── invoke.rs ├── main.rs ├── modals │ ├── add_custom_query.rs │ ├── add_pgsql_connection.rs │ └── mod.rs ├── performane.rs ├── record_view.rs ├── sidebar │ ├── index.rs │ ├── mod.rs │ ├── queries.rs │ └── query.rs └── store │ ├── atoms.rs │ ├── mod.rs │ ├── projects.rs │ ├── queries.rs │ └── tabs.rs └── tailwind.config.js /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-pc-windows-msvc] 2 | linker = "lld" 3 | rustflags = [ 4 | "-Lnative=Users/danixx/.xwin/crt/lib/x86_64", 5 | "-Lnative=Users/danixx/.xwin/sdk/lib/um/x86_64", 6 | "-Lnative=Users/danixx/.xwin/sdk/lib/ucrt/x86_64" 7 | ] 8 | 9 | [target.armv7-unknown-linux-gnueabihf] 10 | linker = "arm-linux-gnueabihf-gcc" 11 | 12 | [target.aarch64-unknown-linux-gnu] 13 | linker = "aarch64-linux-gnu-gcc" -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | release: 10 | permissions: 11 | contents: write 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | platform: [macos-latest, ubuntu-22.04, windows-latest] 16 | runs-on: ${{ matrix.platform }} 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: setup node 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | 26 | - name: install Rust 27 | uses: dtolnay/rust-toolchain@stable 28 | with: 29 | toolchain: nightly-2024-05-04 # 1.80.0-nightly (e82c861d7 2024-05-04) 30 | targets: wasm32-unknown-unknown,${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} 31 | 32 | - name: install Tauri cli 33 | run: cargo install tauri-cli 34 | 35 | - name: install Tailwind CSS 36 | run: npm i -g tailwindcss 37 | 38 | - name: generate Tailwind CSS 39 | run: npx tailwindcss -i ./input.css -o ./style/output.css 40 | 41 | - uses: jetli/trunk-action@v0.1.0 42 | with: 43 | # Optional version of trunk to install(eg. 'v0.8.1', 'latest') 44 | version: "latest" 45 | 46 | - name: install dependencies (ubuntu only) 47 | if: matrix.platform == 'ubuntu-22.04' 48 | run: | 49 | sudo apt-get update 50 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev 51 | 52 | - name: Rust cache 53 | uses: swatinem/rust-cache@v2 54 | with: 55 | workspaces: "./src-tauri -> target" 56 | 57 | - name: build in release mode 58 | run: cargo tauri build 59 | 60 | - uses: tauri-apps/tauri-action@v0 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags. 65 | releaseName: "RSQL v__VERSION__" # tauri-action replaces \_\_VERSION\_\_ with the app version. 66 | releaseBody: "See the assets to download and install this version." 67 | releaseDraft: true 68 | prerelease: false 69 | -------------------------------------------------------------------------------- /.github/workflows/rust-clippy.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # rust-clippy is a tool that runs a bunch of lints to catch common 6 | # mistakes in your Rust code and help improve your Rust code. 7 | # More details at https://github.com/rust-lang/rust-clippy 8 | # and https://rust-lang.github.io/rust-clippy/ 9 | 10 | name: rust-clippy analyze 11 | 12 | on: 13 | push: 14 | branches: [ "main" ] 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: [ "main" ] 18 | schedule: 19 | - cron: '35 7 * * 1' 20 | 21 | jobs: 22 | rust-clippy-analyze: 23 | name: Run rust-clippy analyzing 24 | runs-on: ubuntu-latest 25 | container: 26 | image: rust:latest 27 | permissions: 28 | contents: read 29 | security-events: write 30 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v2 34 | 35 | - name: Install Rust toolchain 36 | uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af #@v1 37 | with: 38 | profile: minimal 39 | toolchain: stable 40 | components: clippy 41 | override: true 42 | 43 | - name: Install required cargo 44 | run: cargo install clippy-sarif sarif-fmt 45 | 46 | - name: Run rust-clippy 47 | run: 48 | cargo clippy 49 | --all-features 50 | --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt 51 | continue-on-error: true 52 | 53 | - name: Upload analysis results to GitHub 54 | uses: github/codeql-action/upload-sarif@v1 55 | with: 56 | sarif_file: rust-clippy-results.sarif 57 | wait-for-processing: true 58 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | # add rust nightly 17 | container: 18 | image: rustlang/rust:nightly 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Build 23 | run: cargo build --verbose 24 | - name: Run tests 25 | run: cargo test --verbose 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /target/ 3 | /Cargo.lock 4 | 5 | /node_modules/ 6 | 7 | /style/ 8 | /.xwin-cache/ 9 | 10 | # Environment file 11 | .env 12 | 13 | -------------------------------------------------------------------------------- /.taurignore: -------------------------------------------------------------------------------- 1 | /src 2 | /public 3 | /Cargo.toml -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Tauri Development Debug", 11 | "cargo": { 12 | "args": ["build", "--manifest-path=./src-tauri/Cargo.toml", "--no-default-features"] 13 | }, 14 | // task for the `beforeDevCommand` if used, must be configured in `.vscode/tasks.json` 15 | "preLaunchTask": "ui:dev" 16 | }, 17 | { 18 | "type": "lldb", 19 | "request": "launch", 20 | "name": "Tauri Production Debug", 21 | "cargo": { 22 | "args": ["build", "--release", "--manifest-path=./src-tauri/Cargo.toml"] 23 | }, 24 | // task for the `beforeBuildCommand` if used, must be configured in `.vscode/tasks.json` 25 | "preLaunchTask": "ui:build" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.includeLanguages": { 3 | "rust": "html" 4 | }, 5 | "editor.quickSuggestions": { 6 | "other": "on", 7 | "comments": "on", 8 | "strings": true 9 | }, 10 | "css.validate": false, 11 | "editor.formatOnSave": true, 12 | "[rust]": { 13 | "editor.semanticHighlighting.enabled": false 14 | }, 15 | "rust-analyzer.rustfmt.overrideCommand": ["leptosfmt", "--stdin", "--rustfmt"], 16 | "rust-analyzer.procMacro.attributes.enable": true, 17 | "editor.tokenColorCustomizations": { 18 | "[LaserWave High Contrast]": { 19 | "textMateRules": [ 20 | { 21 | "scope": "variable.other.rust", 22 | "settings": {} 23 | } 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "ui:dev", 8 | "type": "shell", 9 | // `dev` keeps running in the background 10 | // ideally you should also configure a `problemMatcher` 11 | // see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson 12 | "isBackground": true, 13 | // change this to your `beforeDevCommand`: 14 | "command": "cargo", 15 | "args": ["tauri", "dev"] 16 | }, 17 | { 18 | "label": "ui:build", 19 | "type": "shell", 20 | // change this to your `beforeBuildCommand`: 21 | "command": "cargo", 22 | "args": ["tauri", "build"] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsql" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [dependencies] 8 | leptos = { version = "0.6", features = ["csr", "nightly"] } 9 | leptos_devtools = { git = "https://github.com/luoxiaozero/leptos-devtools" } 10 | serde = { version = "1.0.192", features = ["derive"] } 11 | wasm-bindgen = { version = "0.2.91", features = ["serde-serialize"] } 12 | leptos-use = { version = "^0.13.4" } 13 | leptos_icons = "0.3.0" # https://carlosted.github.io/icondata/ 14 | monaco = "0.4.0" 15 | tauri-sys = { git = "https://github.com/JonasKruckenberg/tauri-sys", features = [ 16 | "all", 17 | ], branch = "v2" } 18 | thaw = { version = "0.3", features = ["csr"] } 19 | common = { path = "common" } 20 | futures = "0.3.30" 21 | icondata = "0.3.0" 22 | ahash = { version = "0.8.11", features = ["serde"] } 23 | leptos_toaster = { version = "0.1.6", features = ["builtin_toast"] } 24 | chrono = "0.4.38" 25 | proc-macros = { path = "./proc-macros" } 26 | 27 | 28 | [workspace] 29 | members = ["src-tauri", "common", "proc-macros"] 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tauri-Leptos PostgreSQL GUI 2 | 3 | ## Introduction 4 | 5 | Welcome to the Tauri-Leptos PostgreSQL GUI, a fully Rust-based application for efficient and secure database management. This application leverages the power of Rust for both frontend and backend development, providing a lightweight and cross-platform solution for PostgreSQL database management. 6 | 7 | ## Feature Roadmap 8 | 9 | ### To-Do List 10 | 11 | - [x] **Connection Data Storage:** Implement secure storage for database connection details. 12 | - [x] **Multi-Connection Support:** Enable the GUI to handle multiple database connections simultaneously. 13 | - [x] **SQL Query Saving:** Allow users to save and manage their frequently used SQL queries. 14 | - [x] **Grid and Record View:** Provide a grid view for query results, as well as a record view for individual records. 15 | - [x] **Multi Tab Support:** Allow users to open multiple tabs for SQL queries. 16 | - [ ] **BigQuery Support:** Add support for BigQuery databases. 17 | 18 | ## Key Features 19 | 20 | - **Pure Rust Development:** Entirely written in Rust, offering safety and performance. 21 | - **Lightweight and Secure:** Built with Tauri, ensuring a secure and resource-efficient desktop application. 22 | - **PostgreSQL Integration:** Tailored for effective management of PostgreSQL databases. 23 | 24 | ## Getting Started 25 | 26 | ### Prerequisites 27 | 28 | - Rust Environment: Ensure Rust is installed on your system. Install from [rustup.rs](https://rustup.rs/). 29 | - Tauri Setup: Set up your development environment for Tauri following the [Tauri setup guide](https://tauri.app/v1/guides/getting-started/prerequisites). 30 | - Install Tauri CLI: Run `cargo install tauri-cli` to install the Tauri CLI. 31 | 32 | ### Installation and Running 33 | 34 | 1. **Clone the Repository:** Download the project to your local machine. 35 | 2. **Build and Run:** Execute `cargo tauri dev` to build the application. Once the build is complete, you can start the application. 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Trunk.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "./index.html" 3 | 4 | [watch] 5 | ignore = ["./src-tauri"] 6 | 7 | [serve] 8 | address = "127.0.0.1" 9 | port = 1420 10 | open = false 11 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "common" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | serde = "1.0.193" 8 | -------------------------------------------------------------------------------- /common/src/enums.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Clone, Copy, Serialize, Deserialize, Default)] 6 | pub enum Drivers { 7 | #[default] 8 | PGSQL, 9 | } 10 | 11 | impl Display for Drivers { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | match self { 14 | Drivers::PGSQL => write!(f, "PGSQL"), 15 | } 16 | } 17 | } 18 | 19 | impl AsRef for Drivers { 20 | fn as_ref(&self) -> &str { 21 | match self { 22 | Drivers::PGSQL => "PGSQL", 23 | } 24 | } 25 | } 26 | 27 | #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] 28 | pub enum ProjectConnectionStatus { 29 | Connected, 30 | Connecting, 31 | #[default] 32 | Disconnected, 33 | Failed, 34 | } 35 | 36 | impl std::error::Error for ProjectConnectionStatus {} 37 | impl Display for ProjectConnectionStatus { 38 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 39 | match self { 40 | ProjectConnectionStatus::Connected => write!(f, "Connected"), 41 | ProjectConnectionStatus::Connecting => write!(f, "Connecting"), 42 | ProjectConnectionStatus::Disconnected => write!(f, "Disconnected"), 43 | ProjectConnectionStatus::Failed => write!(f, "Failed"), 44 | } 45 | } 46 | } 47 | 48 | use std::fmt; 49 | 50 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 51 | pub enum PostgresqlError { 52 | ConnectionTimeout, 53 | ConnectionError, 54 | QueryTimeout, 55 | QueryError, 56 | } 57 | 58 | impl std::error::Error for PostgresqlError {} 59 | impl fmt::Display for PostgresqlError { 60 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 61 | match *self { 62 | PostgresqlError::ConnectionTimeout => write!(f, "ConnectionTimeout"), 63 | PostgresqlError::ConnectionError => write!(f, "ConnectionError"), 64 | PostgresqlError::QueryTimeout => write!(f, "QueryTimeout"), 65 | PostgresqlError::QueryError => write!(f, "QueryError"), 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod enums; 2 | pub mod types; 3 | 4 | -------------------------------------------------------------------------------- /common/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | pub mod pgsql; 4 | 5 | pub type BTreeStore = BTreeMap; 6 | pub type BTreeVecStore = BTreeMap>; 7 | 8 | -------------------------------------------------------------------------------- /common/src/types/pgsql.rs: -------------------------------------------------------------------------------- 1 | pub type PgsqlLoadSchemas = Vec; 2 | pub type PgsqlLoadTables = Vec<(String, String)>; 3 | pub type PgsqlRunQuery = (Vec, Vec>, f32); 4 | 5 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tauri + Leptos App 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /leptosfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 # Maximum width of each line 2 | tab_spaces = 4 # Number of spaces per tab 3 | indentation_style = "Auto" # "Tabs", "Spaces" or "Auto" 4 | newline_style = "Auto" # "Unix", "Windows" or "Auto" 5 | attr_value_brace_style = "WhenRequired" # "Always", "AlwaysUnlessLit", "WhenRequired" or "Preserve" -------------------------------------------------------------------------------- /proc-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proc-macros" 3 | version = "1.0.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | proc-macro2 = "1.0.86" 8 | quote = "1.0.37" 9 | syn = "2.0.77" 10 | 11 | [lib] 12 | name = "rsql_proc_macros" 13 | proc-macro = true 14 | -------------------------------------------------------------------------------- /proc-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{parse_macro_input, Data, DeriveInput, Fields, ItemFn}; 6 | 7 | #[proc_macro_attribute] 8 | pub fn set_running_query(_attr: TokenStream, item: TokenStream) -> TokenStream { 9 | let input = parse_macro_input!(item as ItemFn); 10 | 11 | let vis = &input.vis; 12 | let sig = &input.sig; 13 | let block = &input.block; 14 | 15 | let gen = quote! { 16 | #vis #sig { 17 | let run_query_atom = expect_context::(); 18 | run_query_atom.set(RunQueryAtom { is_running: true }); 19 | 20 | let result = async { 21 | #block 22 | }.await; 23 | 24 | run_query_atom.set(RunQueryAtom { is_running: false }); 25 | 26 | result 27 | } 28 | }; 29 | 30 | TokenStream::from(gen) 31 | } 32 | 33 | #[proc_macro_derive(StructIntoIterator)] 34 | pub fn into_iterator_derive(input: TokenStream) -> TokenStream { 35 | let input = parse_macro_input!(input as DeriveInput); 36 | 37 | let name = &input.ident; 38 | 39 | let fields = match input.data { 40 | Data::Struct(data_struct) => match data_struct.fields { 41 | Fields::Named(fields_named) => fields_named.named, 42 | _ => panic!("IntoIterator can only be derived for structs with named fields."), 43 | }, 44 | _ => panic!("IntoIterator can only be derived for structs."), 45 | }; 46 | 47 | let field_names = fields.iter().filter_map(|f| { 48 | let field_type = &f.ty; 49 | if let syn::Type::Path(type_path) = field_type { 50 | if type_path 51 | .path 52 | .segments 53 | .iter() 54 | .any(|segment| segment.ident == "String") 55 | { 56 | return f.ident.as_ref(); 57 | } 58 | } 59 | None 60 | }); 61 | 62 | let gen = quote! { 63 | impl IntoIterator for #name { 64 | type Item = String; 65 | type IntoIter = std::vec::IntoIter; 66 | 67 | fn into_iter(self) -> Self::IntoIter { 68 | let mut vec = Vec::new(); 69 | #( 70 | vec.push(self.#field_names.to_owned()); 71 | )* 72 | vec.into_iter() 73 | } 74 | } 75 | }; 76 | 77 | gen.into() 78 | } 79 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/public/icon.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | max_width = 100 # Maximum width of each line 3 | tab_spaces = 2 # Number of spaces per tab 4 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | /project_db/ 6 | /query_db/ 7 | /Cache/ 8 | /AppData/ 9 | /projects/ -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "backend" 3 | version = "1.0.0" 4 | description = "PostgreSQL GUI written in Rust" 5 | authors = ["Daniel Boros"] 6 | license = "" 7 | repository = "" 8 | edition = "2021" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [build-dependencies] 13 | tauri-build = { version = "2", features = [] } 14 | 15 | [dependencies] 16 | common = { path = "../common" } 17 | tauri = { version = "2", features = [] } 18 | tokio = "1.37.0" 19 | tokio-postgres = "0.7.10" 20 | chrono = "0.4.31" 21 | sled = "0.34.7" 22 | tracing = "0.1.40" 23 | tracing-subscriber = { version = "0.3.18", features = ["fmt"] } 24 | bincode = "1.3.3" 25 | tauri-plugin-shell = "2" 26 | tauri-plugin-fs = "2" 27 | serde_json = "1.0.133" 28 | 29 | 30 | [features] 31 | # this feature is used for production builds or when `devPath` points to the filesystem 32 | # DO NOT REMOVE!! 33 | custom-protocol = ["tauri/custom-protocol"] 34 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/migrated.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "migrated", 3 | "description": "permissions that were migrated from v1", 4 | "local": true, 5 | "windows": [ 6 | "main" 7 | ], 8 | "permissions": [ 9 | "core:default", 10 | "fs:allow-read-file", 11 | "fs:allow-write-file", 12 | "fs:allow-read-dir", 13 | "fs:allow-copy-file", 14 | "fs:allow-mkdir", 15 | "fs:allow-remove", 16 | "fs:allow-remove", 17 | "fs:allow-rename", 18 | "fs:allow-exists", 19 | { 20 | "identifier": "fs:scope", 21 | "allow": [ 22 | "$APPDATA/*" 23 | ] 24 | }, 25 | "shell:allow-open", 26 | "shell:default", 27 | "fs:default" 28 | ] 29 | } -------------------------------------------------------------------------------- /src-tauri/gen/schemas/capabilities.json: -------------------------------------------------------------------------------- 1 | {"migrated":{"identifier":"migrated","description":"permissions that were migrated from v1","local":true,"windows":["main"],"permissions":["core:default","fs:allow-read-file","fs:allow-write-file","fs:allow-read-dir","fs:allow-copy-file","fs:allow-mkdir","fs:allow-remove","fs:allow-remove","fs:allow-rename","fs:allow-exists",{"identifier":"fs:scope","allow":["$APPDATA/*"]},"shell:allow-open","shell:default","fs:default"]}} -------------------------------------------------------------------------------- /src-tauri/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/128x128.png -------------------------------------------------------------------------------- /src-tauri/icons/128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/128x128@2x.png -------------------------------------------------------------------------------- /src-tauri/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/32x32.png -------------------------------------------------------------------------------- /src-tauri/icons/Square107x107Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square107x107Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square142x142Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square142x142Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square150x150Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square150x150Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square284x284Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square284x284Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square30x30Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square30x30Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square310x310Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square310x310Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square44x44Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square44x44Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square71x71Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square71x71Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/Square89x89Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/Square89x89Logo.png -------------------------------------------------------------------------------- /src-tauri/icons/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/StoreLogo.png -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-dd/rust-sql/932a90e6d470d8d8ed3e3b202afe888bcb97d45f/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/src/dbs/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod project; 2 | pub mod query; 3 | -------------------------------------------------------------------------------- /src-tauri/src/dbs/project.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use tauri::{Result, State}; 4 | 5 | use crate::AppState; 6 | use common::types::BTreeVecStore; 7 | 8 | #[tauri::command(rename_all = "snake_case")] 9 | pub async fn project_db_select(app_state: State<'_, AppState>) -> Result { 10 | let project_db = app_state.project_db.lock().await; 11 | let db = project_db.clone().unwrap(); 12 | let mut projects = BTreeMap::new(); 13 | 14 | if db.is_empty() { 15 | tracing::info!("No projects found in the database"); 16 | return Ok(projects); 17 | } 18 | 19 | for p in db.iter() { 20 | let project = p.unwrap(); 21 | 22 | let project = ( 23 | String::from_utf8(project.0.to_vec()).unwrap(), 24 | bincode::deserialize(&project.1).unwrap(), 25 | ); 26 | projects.insert(project.0, project.1); 27 | } 28 | Ok(projects) 29 | } 30 | 31 | #[tauri::command(rename_all = "snake_case")] 32 | pub async fn project_db_insert( 33 | project_id: &str, 34 | project_details: Vec, 35 | app_state: State<'_, AppState>, 36 | ) -> Result<()> { 37 | let project_db = app_state.project_db.lock().await; 38 | let db = project_db.clone().unwrap(); 39 | let project_details = bincode::serialize(&project_details).unwrap(); 40 | db.insert(project_id, project_details).unwrap(); 41 | Ok(()) 42 | } 43 | 44 | #[tauri::command(rename_all = "snake_case")] 45 | pub async fn project_db_delete(project_id: &str, app_state: State<'_, AppState>) -> Result<()> { 46 | let db = app_state.project_db.lock().await; 47 | let db = db.clone().unwrap(); 48 | db.remove(project_id).unwrap(); 49 | Ok(()) 50 | } 51 | -------------------------------------------------------------------------------- /src-tauri/src/dbs/query.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use tauri::{AppHandle, Manager, Result, State}; 4 | 5 | use crate::AppState; 6 | 7 | #[tauri::command(rename_all = "snake_case")] 8 | pub async fn query_db_select(app_state: State<'_, AppState>) -> Result> { 9 | let query_db = app_state.query_db.lock().await; 10 | let mut queries = BTreeMap::new(); 11 | if let Some(ref query_db) = *query_db { 12 | for query in query_db.iter() { 13 | let (key, value) = query.unwrap(); 14 | let key = String::from_utf8(key.to_vec()).unwrap(); 15 | let value = String::from_utf8(value.to_vec()).unwrap(); 16 | queries.insert(key, value); 17 | } 18 | }; 19 | Ok(queries) 20 | } 21 | 22 | #[tauri::command(rename_all = "snake_case")] 23 | pub async fn query_db_insert(query_id: &str, sql: &str, app: AppHandle) -> Result<()> { 24 | let app_state = app.state::(); 25 | let db = app_state.query_db.lock().await; 26 | if let Some(ref db_instance) = *db { 27 | db_instance.insert(query_id, sql).unwrap(); 28 | } 29 | Ok(()) 30 | } 31 | 32 | #[tauri::command(rename_all = "snake_case")] 33 | pub async fn query_db_delete(query_id: &str, app_state: State<'_, AppState>) -> Result<()> { 34 | let query_db = app_state.query_db.lock().await; 35 | if let Some(ref query_db) = *query_db { 36 | query_db.remove(query_id).unwrap(); 37 | }; 38 | Ok(()) 39 | } 40 | -------------------------------------------------------------------------------- /src-tauri/src/drivers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pgsql; 2 | -------------------------------------------------------------------------------- /src-tauri/src/drivers/pgsql.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::Arc, time::Instant}; 2 | 3 | use common::{ 4 | enums::{PostgresqlError, ProjectConnectionStatus}, 5 | types::pgsql::{PgsqlLoadSchemas, PgsqlLoadTables}, 6 | }; 7 | use tauri::{AppHandle, Manager, Result, State}; 8 | use tokio::{sync::Mutex, time as tokio_time}; 9 | use tokio_postgres::{connect, NoTls}; 10 | 11 | use crate::{utils::reflective_get, AppState}; 12 | 13 | #[tauri::command(rename_all = "snake_case")] 14 | pub async fn pgsql_connector( 15 | project_id: &str, 16 | key: Option<[&str; 5]>, 17 | app: AppHandle, 18 | ) -> Result { 19 | let app_state = app.state::(); 20 | let mut clients = app_state.client.lock().await; 21 | 22 | // check if connection already exists 23 | if clients.as_ref().unwrap().contains_key(project_id) { 24 | tracing::info!("Postgres connection already exists!"); 25 | return Ok(ProjectConnectionStatus::Connected); 26 | } 27 | 28 | let key = match key { 29 | Some(key) => format!( 30 | "postgresql://{}:{}@{}:{}/{}", 31 | key[0], key[1], key[3], key[4], key[2] 32 | ), 33 | None => { 34 | let projects_db = app_state.project_db.lock().await; 35 | let projects_db = projects_db.as_ref().unwrap(); 36 | let project_details = projects_db.get(project_id).unwrap(); 37 | let project_details = match project_details { 38 | Some(bytes) => bincode::deserialize::>(&bytes).unwrap(), 39 | _ => Vec::new(), 40 | }; 41 | let project_details = format!( 42 | "postgresql://{}:{}@{}:{}/{}", 43 | project_details[1], 44 | project_details[2], 45 | project_details[4], 46 | project_details[5], 47 | project_details[3], 48 | ); 49 | project_details 50 | } 51 | }; 52 | 53 | let connection = tokio_time::timeout(tokio_time::Duration::from_secs(10), connect(&key, NoTls)) 54 | .await 55 | .map_err(|_| PostgresqlError::ConnectionTimeout); 56 | 57 | if connection.is_err() { 58 | tracing::error!("Postgres connection timeout error!"); 59 | return Ok(ProjectConnectionStatus::Failed); 60 | } 61 | 62 | let connection = connection.unwrap(); 63 | if connection.is_err() { 64 | tracing::error!("Postgres connection error!"); 65 | return Ok(ProjectConnectionStatus::Failed); 66 | } 67 | 68 | let is_connection_error = Arc::new(Mutex::new(false)); 69 | let (client, connection) = connection.unwrap(); 70 | tracing::info!("Postgres connection established!"); 71 | 72 | // check if connection has some error 73 | tokio::spawn({ 74 | let is_connection_error = Arc::clone(&is_connection_error); 75 | async move { 76 | if let Err(e) = connection.await { 77 | tracing::info!("Postgres connection error: {:?}", e); 78 | *is_connection_error.lock().await = true; 79 | } 80 | } 81 | }); 82 | 83 | if *is_connection_error.lock().await { 84 | tracing::error!("Postgres connection error!"); 85 | return Ok(ProjectConnectionStatus::Failed); 86 | } 87 | 88 | let clients = clients.as_mut().unwrap(); 89 | clients.insert(project_id.to_string(), client); 90 | 91 | Ok(ProjectConnectionStatus::Connected) 92 | } 93 | 94 | #[tauri::command(rename_all = "snake_case")] 95 | pub async fn pgsql_load_schemas( 96 | project_id: &str, 97 | app_state: State<'_, AppState>, 98 | ) -> Result { 99 | let clients = app_state.client.lock().await; 100 | let client = clients.as_ref().unwrap().get(project_id).unwrap(); 101 | 102 | let query = tokio_time::timeout( 103 | tokio_time::Duration::from_secs(10), 104 | client.query( 105 | r#" 106 | SELECT schema_name 107 | FROM information_schema.schemata 108 | WHERE schema_name NOT IN ('pg_catalog', 'information_schema') 109 | ORDER BY schema_name; 110 | "#, 111 | &[], 112 | ), 113 | ) 114 | .await 115 | .map_err(|_| PostgresqlError::QueryTimeout); 116 | 117 | if query.is_err() { 118 | tracing::error!("Postgres schema query timeout error!"); 119 | return Err(tauri::Error::Io(std::io::Error::new( 120 | std::io::ErrorKind::Other, 121 | PostgresqlError::QueryTimeout, 122 | ))); 123 | } 124 | 125 | let query = query.unwrap(); 126 | if query.is_err() { 127 | tracing::error!("Postgres schema query error!"); 128 | return Err(tauri::Error::Io(std::io::Error::new( 129 | std::io::ErrorKind::Other, 130 | PostgresqlError::QueryError, 131 | ))); 132 | } 133 | 134 | let qeury = query.unwrap(); 135 | let schemas = qeury.iter().map(|r| r.get(0)).collect::>(); 136 | tracing::info!("Postgres schemas: {:?}", schemas); 137 | Ok(schemas) 138 | } 139 | 140 | #[tauri::command(rename_all = "snake_case")] 141 | pub async fn pgsql_load_tables( 142 | project_id: &str, 143 | schema: &str, 144 | app_state: State<'_, AppState>, 145 | ) -> Result { 146 | let clients = app_state.client.lock().await; 147 | let client = clients.as_ref().unwrap().get(project_id).unwrap(); 148 | let query = client 149 | .query( 150 | r#"--sql 151 | SELECT 152 | table_name, 153 | pg_size_pretty(pg_total_relation_size('"' || table_schema || '"."' || table_name || '"')) AS size 154 | FROM 155 | information_schema.tables 156 | WHERE 157 | table_schema = $1 158 | ORDER BY 159 | table_name; 160 | "#, 161 | &[&schema], 162 | ) 163 | .await 164 | .unwrap(); 165 | let tables = query 166 | .iter() 167 | .map(|r| (r.get(0), r.get(1))) 168 | .collect::>(); 169 | Ok(tables) 170 | } 171 | 172 | #[tauri::command(rename_all = "snake_case")] 173 | pub async fn pgsql_run_query( 174 | project_id: &str, 175 | sql: &str, 176 | app_state: State<'_, AppState>, 177 | ) -> Result<(Vec, Vec>, f32)> { 178 | let start = Instant::now(); 179 | let clients = app_state.client.lock().await; 180 | let client = clients.as_ref().unwrap().get(project_id).unwrap(); 181 | let rows = client.query(sql, &[]).await.unwrap(); 182 | 183 | if rows.is_empty() { 184 | return Ok((Vec::new(), Vec::new(), 0.0f32)); 185 | } 186 | 187 | let columns = rows 188 | .first() 189 | .unwrap() 190 | .columns() 191 | .iter() 192 | .map(|c| c.name().to_string()) 193 | .collect::>(); 194 | let rows = rows 195 | .iter() 196 | .map(|row| { 197 | let mut row_values = Vec::new(); 198 | for i in 0..row.len() { 199 | let value = reflective_get(row, i); 200 | row_values.push(value); 201 | } 202 | row_values 203 | }) 204 | .collect::>>(); 205 | let elasped = start.elapsed().as_millis() as f32; 206 | Ok((columns, rows, elasped)) 207 | } 208 | 209 | #[tauri::command(rename_all = "snake_case")] 210 | pub async fn pgsql_load_relations( 211 | project_name: &str, 212 | schema: &str, 213 | app_state: State<'_, AppState>, 214 | ) -> Result> { 215 | // let clients = app_state.client.lock().await; 216 | // let client = clients.as_ref().unwrap().get(project_name).unwrap(); 217 | // let rows = client 218 | // .query( 219 | // r#"--sql SELECT 220 | // tc.constraint_name, 221 | // tc.table_name, 222 | // kcu.column_name, 223 | // ccu.table_name AS foreign_table_name, 224 | // ccu.column_name AS foreign_column_name 225 | // FROM information_schema.table_constraints AS tc 226 | // JOIN information_schema.key_column_usage AS kcu 227 | // ON tc.constraint_name = kcu.constraint_name 228 | // JOIN information_schema.constraint_column_usage AS ccu 229 | // ON ccu.constraint_name = tc.constraint_name 230 | // WHERE constraint_type = 'FOREIGN KEY' 231 | // AND tc.table_schema = $1; 232 | // "#, 233 | // &[&schema], 234 | // ) 235 | // .await 236 | // .unwrap(); 237 | 238 | // let relations = rows 239 | // .iter() 240 | // .map(|row| { 241 | // let constraint_name = row.get(0); 242 | // let table_name = row.get(1); 243 | // let column_name = row.get(2); 244 | // let foreign_table_name = row.get(3); 245 | // let foreign_column_name = row.get(4); 246 | // PostgresqlRelation { 247 | // constraint_name, 248 | // table_name, 249 | // column_name, 250 | // foreign_table_name, 251 | // foreign_column_name, 252 | // } 253 | // }) 254 | // .collect::>(); 255 | 256 | // Ok(relations) 257 | todo!() 258 | } 259 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | // Prevents additional console window on Windows in release, DO NOT REMOVE!! 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 3 | 4 | mod dbs; 5 | mod drivers; 6 | mod utils; 7 | 8 | const PROJECT_DB_PATH: &str = "project_db"; 9 | const QUERY_DB_PATH: &str = "query_db"; 10 | 11 | use sled::Db; 12 | use std::{collections::BTreeMap, sync::Arc}; 13 | use tauri::Manager; 14 | use tokio::sync::Mutex; 15 | use tokio_postgres::Client; 16 | use tracing::Level; 17 | use utils::create_or_open_local_db; 18 | 19 | pub struct AppState { 20 | pub client: Arc>>>, 21 | pub project_db: Arc>>, 22 | pub query_db: Arc>>, 23 | } 24 | 25 | impl Default for AppState { 26 | fn default() -> Self { 27 | Self { 28 | client: Arc::new(Mutex::new(Some(BTreeMap::new()))), 29 | project_db: Arc::new(Mutex::new(None)), 30 | query_db: Arc::new(Mutex::new(None)), 31 | } 32 | } 33 | } 34 | 35 | fn main() { 36 | tracing_subscriber::fmt() 37 | .with_file(true) 38 | .with_line_number(true) 39 | .with_level(true) 40 | .with_max_level(Level::INFO) 41 | .init(); 42 | 43 | tauri::Builder::default() 44 | .plugin(tauri_plugin_fs::init()) 45 | .plugin(tauri_plugin_shell::init()) 46 | .manage(AppState::default()) 47 | .setup(|app| { 48 | let app_handle = app.handle(); 49 | 50 | tauri::async_runtime::block_on(async move { 51 | let app_dir = app_handle.path().app_data_dir().unwrap(); 52 | let app_state = app_handle.state::(); 53 | let project_db = create_or_open_local_db(PROJECT_DB_PATH, &app_dir); 54 | let query_db = create_or_open_local_db(QUERY_DB_PATH, &app_dir); 55 | *app_state.project_db.lock().await = Some(project_db); 56 | *app_state.query_db.lock().await = Some(query_db); 57 | }); 58 | 59 | // open devtools if we are in debug mode 60 | #[cfg(debug_assertions)] 61 | { 62 | let window = app.get_webview_window("main").unwrap(); 63 | window.open_devtools(); 64 | window.close_devtools(); 65 | } 66 | 67 | Ok(()) 68 | }) 69 | .invoke_handler(tauri::generate_handler![ 70 | dbs::project::project_db_select, 71 | dbs::project::project_db_insert, 72 | dbs::project::project_db_delete, 73 | dbs::query::query_db_select, 74 | dbs::query::query_db_insert, 75 | dbs::query::query_db_delete, 76 | drivers::pgsql::pgsql_connector, 77 | drivers::pgsql::pgsql_load_relations, 78 | drivers::pgsql::pgsql_load_schemas, 79 | drivers::pgsql::pgsql_load_tables, 80 | drivers::pgsql::pgsql_run_query, 81 | ]) 82 | .run(tauri::generate_context!()) 83 | .expect("error while running tauri application"); 84 | } 85 | -------------------------------------------------------------------------------- /src-tauri/src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::{path::Path, time::SystemTime}; 2 | 3 | use chrono::DateTime; 4 | use tokio_postgres::Row; 5 | 6 | pub fn create_or_open_local_db(path: &str, app_dir: &Path) -> sled::Db { 7 | if cfg!(debug_assertions) { 8 | let db = sled::open(path).unwrap(); 9 | return db; 10 | } 11 | 12 | let db_path = app_dir.join(path); 13 | sled::open(db_path).unwrap() 14 | } 15 | 16 | /// The postgres-crate does not provide a default mapping to fallback to String for all 17 | /// types: row.get is generic and without a type assignment the FromSql-Trait cannot be inferred. 18 | /// This function matches over the current column-type and does a manual conversion 19 | pub fn reflective_get(row: &Row, index: usize) -> String { 20 | let column_type = row.columns().get(index).map(|c| c.type_().name()).unwrap(); 21 | // see https://docs.rs/sqlx/0.4.0-beta.1/sqlx/postgres/types/index.html 22 | 23 | let value = match column_type { 24 | "bool" => { 25 | let v = row.get::<_, Option>(index); 26 | v.map(|v| v.to_string()) 27 | } 28 | "varchar" | "char(n)" | "text" | "name" => row.get(index), 29 | "char" => { 30 | let v = row.get::<_, i8>(index); 31 | Some(v.to_string()) 32 | } 33 | "smallserial" | "smallint" => { 34 | let v = row.get::<_, Option>(index); 35 | v.map(|v| v.to_string()) 36 | } 37 | "int" | "int4" | "serial" => { 38 | let v = row.get::<_, Option>(index); 39 | v.map(|v| v.to_string()) 40 | } 41 | "int8" | "bigserial" | "bigint" => { 42 | let v = row.get::<_, Option>(index); 43 | v.map(|v| v.to_string()) 44 | } 45 | "float4" | "real" => { 46 | let v = row.get::<_, Option>(index); 47 | v.map(|v| v.to_string()) 48 | } 49 | "float8" | "double precision" => { 50 | let v = row.get::<_, Option>(index); 51 | v.map(|v| v.to_string()) 52 | } 53 | "timestamp" | "timestamptz" => { 54 | // with-chrono feature is needed for this 55 | let v: Option = row.get(index); 56 | let v = DateTime::::from(v.unwrap()); 57 | Some(v.to_string()) 58 | } 59 | &_ => Some("CANNOT PARSE".to_string()), 60 | }; 61 | value.unwrap_or("null".to_string()) 62 | } 63 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "trunk serve", 4 | "beforeBuildCommand": "trunk build", 5 | "frontendDist": "../dist", 6 | "devUrl": "http://localhost:1420" 7 | }, 8 | "bundle": { 9 | "active": true, 10 | "targets": "all", 11 | "icon": [ 12 | "icons/32x32.png", 13 | "icons/128x128.png", 14 | "icons/128x128@2x.png", 15 | "icons/icon.icns", 16 | "icons/icon.ico" 17 | ] 18 | }, 19 | "productName": "RSQL", 20 | "mainBinaryName": "RSQL", 21 | "version": "1.0.0", 22 | "identifier": "com.rsql.app", 23 | "plugins": {}, 24 | "app": { 25 | "withGlobalTauri": true, 26 | "windows": [ 27 | { 28 | "fullscreen": false, 29 | "resizable": true, 30 | "title": "RSQL", 31 | "width": 1920, 32 | "height": 1080, 33 | "useHttpsScheme": true 34 | } 35 | ], 36 | "security": { 37 | "csp": null 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use leptos::*; 4 | use leptos_toaster::{Toaster, ToasterPosition}; 5 | 6 | use crate::{ 7 | dashboard::index::Dashboard, 8 | enums::QueryTableLayout, 9 | footer::Footer, 10 | performane::Performance, 11 | sidebar::index::Sidebar, 12 | store::{ 13 | atoms::{ 14 | PgsqlConnectionDetailsAtom, PgsqlConnectionDetailsContext, QueryPerformanceAtom, 15 | QueryPerformanceContext, RunQueryAtom, RunQueryContext, 16 | }, 17 | projects::ProjectsStore, 18 | queries::QueriesStore, 19 | tabs::TabsStore, 20 | }, 21 | }; 22 | 23 | // TODO: help to add custom langunage support 24 | // https://github.com/abesto/clox-rs/blob/def4bed61a1c1c6b5d84a67284549a6343c8cd06/web/src/monaco_lox.rs 25 | 26 | #[component] 27 | pub fn App() -> impl IntoView { 28 | provide_context(ProjectsStore::new()); 29 | provide_context(QueriesStore::new()); 30 | provide_context(RwSignal::new(QueryTableLayout::Grid)); 31 | provide_context::( 32 | RwSignal::new(VecDeque::::new()), 33 | ); 34 | provide_context::(RwSignal::new(RunQueryAtom::default())); 35 | provide_context::(RwSignal::new( 36 | PgsqlConnectionDetailsAtom::default(), 37 | )); 38 | provide_context(TabsStore::default()); 39 | 40 | view! { 41 | 42 |
43 | 44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/dashboard/index.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_icons::Icon; 3 | use thaw::{Tab, TabLabel, Tabs}; 4 | 5 | use crate::store::tabs; 6 | 7 | use super::{query_editor::QueryEditor, query_table::QueryTable}; 8 | 9 | #[component] 10 | pub fn Dashboard() -> impl IntoView { 11 | let tabs_store = expect_context::(); 12 | 13 | view! { 14 | 15 | 21 | 22 |
23 | {format!("Tab {}", index + 1)} 24 | 34 |
35 |
36 | 37 | 38 | 39 | } 40 | } 41 | /> 42 | 43 |
44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/dashboard/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod index; 2 | pub mod query_editor; 3 | pub mod query_table; 4 | 5 | -------------------------------------------------------------------------------- /src/dashboard/query_editor.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc, sync::Arc}; 2 | 3 | use futures::lock::Mutex; 4 | use leptos::*; 5 | use leptos_use::{use_document, use_event_listener}; 6 | use monaco::{ 7 | api::{CodeEditor, CodeEditorOptions, TextModel}, 8 | sys::{ 9 | editor::{IDimension, IEditorMinimapOptions}, 10 | KeyCode, KeyMod, 11 | }, 12 | }; 13 | use wasm_bindgen::{closure::Closure, JsCast}; 14 | 15 | use crate::{ 16 | modals::add_custom_query::AddCustomQuery, 17 | store::{projects::ProjectsStore, tabs::TabsStore}, 18 | }; 19 | 20 | pub type ModelCell = Rc>>; 21 | pub const MODE_ID: &str = "pgsql"; 22 | 23 | #[component] 24 | pub fn QueryEditor(index: usize) -> impl IntoView { 25 | let tabs_store = expect_context::(); 26 | let active_project = move || tabs_store.selected_projects.get().get(index).cloned(); 27 | let projects_store = expect_context::(); 28 | let project_driver = projects_store.select_driver_by_project(active_project().as_deref()); 29 | let project_database = projects_store.select_database_by_project(active_project().as_deref()); 30 | let tabs_store_rc = Rc::new(RefCell::new(tabs_store)); 31 | let show = create_rw_signal(false); 32 | let _ = use_event_listener(use_document(), ev::keydown, move |event| { 33 | if event.key() == "Escape" { 34 | show.set(false); 35 | } 36 | }); 37 | let node_ref = create_node_ref(); 38 | 39 | { 40 | let tabs_store = tabs_store_rc.clone(); 41 | node_ref.on_load(move |node| { 42 | let div_element: &web_sys::HtmlDivElement = &node; 43 | let html_element = div_element.unchecked_ref::(); 44 | let options = CodeEditorOptions::default().to_sys_options(); 45 | let text_model = 46 | TextModel::create("# Add your SQL query here...", Some(MODE_ID), None).unwrap(); 47 | options.set_model(Some(text_model.as_ref())); 48 | options.set_language(Some(MODE_ID)); 49 | options.set_automatic_layout(Some(true)); 50 | options.set_dimension(Some(&IDimension::new(0, 240))); 51 | let minimap_settings = IEditorMinimapOptions::default(); 52 | minimap_settings.set_enabled(Some(false)); 53 | options.set_minimap(Some(&minimap_settings)); 54 | 55 | let e = CodeEditor::create(html_element, Some(options)); 56 | let keycode = KeyMod::win_ctrl() as u32 | KeyCode::Enter.to_value(); 57 | // TODO: Fix this 58 | e.as_ref().add_command( 59 | keycode.into(), 60 | Closure::::new(|| ()).as_ref().unchecked_ref(), 61 | None, 62 | ); 63 | 64 | // TODO: Fix this 65 | let e = Rc::new(RefCell::new(Some(e))); 66 | tabs_store.borrow_mut().add_editor(e); 67 | }); 68 | }; 69 | 70 | let tabs_store_arc = Arc::new(Mutex::new(tabs_store)); 71 | let run_query = create_action(move |tabs_store: &Arc>| { 72 | let tabs_store = tabs_store.clone(); 73 | async move { 74 | tabs_store.lock().await.run_query().await; 75 | } 76 | }); 77 | 78 | let _ = use_event_listener(node_ref, ev::keydown, { 79 | let tabs_store = tabs_store_arc.clone(); 80 | 81 | move |event| { 82 | if event.key() == "Enter" && event.ctrl_key() { 83 | run_query.dispatch(tabs_store.clone()); 84 | } 85 | } 86 | }); 87 | 88 | view! { 89 |
90 |
91 |
}> 92 | 98 |
99 | {active_project} 100 |
101 | 102 |
}> 103 | 104 |
105 | 111 | 121 |
122 | 123 | 124 | 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /src/dashboard/query_table.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_icons::*; 3 | 4 | use crate::{ 5 | enums::QueryTableLayout, 6 | grid_view::GridView, 7 | record_view::RecordView, 8 | store::{atoms::RunQueryContext, tabs::TabsStore}, 9 | }; 10 | 11 | #[component] 12 | pub fn QueryTable() -> impl IntoView { 13 | let tabs_store = expect_context::(); 14 | let table_view = expect_context::>(); 15 | let is_query_running = expect_context::(); 16 | 17 | view! { 18 | 23 | 29 |

"Running query..."

30 | 31 | } 32 | } 33 | > 34 | 35 | {move || match tabs_store.select_active_editor_sql_result() { 36 | None => { 37 | view! { 38 |
39 | "No data to display" 40 |
41 | } 42 | } 43 | Some(_) => { 44 | view! { 45 |
46 | {match table_view.get() { 47 | QueryTableLayout::Grid => view! { }, 48 | QueryTableLayout::Records => view! { }, 49 | }} 50 | 51 |
52 | } 53 | } 54 | }} 55 | 56 |
57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/databases/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod pgsql; 2 | 3 | -------------------------------------------------------------------------------- /src/databases/pgsql/driver.rs: -------------------------------------------------------------------------------- 1 | use ahash::AHashMap; 2 | use common::{ 3 | enums::ProjectConnectionStatus, 4 | types::pgsql::{PgsqlLoadSchemas, PgsqlLoadTables, PgsqlRunQuery}, 5 | }; 6 | use leptos::{error::Result, expect_context, RwSignal, SignalGet, SignalSet, SignalUpdate}; 7 | use rsql_proc_macros::set_running_query; 8 | use tauri_sys::core::invoke; 9 | 10 | use crate::{ 11 | invoke::{ 12 | Invoke, InvokePgsqlConnectorArgs, InvokePgsqlLoadSchemasArgs, InvokePgsqlLoadTablesArgs, 13 | InvokePgsqlRunQueryArgs, 14 | }, 15 | store::{ 16 | atoms::{ 17 | PgsqlConnectionDetailsContext, QueryPerformanceAtom, QueryPerformanceContext, RunQueryAtom, 18 | RunQueryContext, 19 | }, 20 | tabs::TabsStore, 21 | }, 22 | }; 23 | 24 | #[derive(Debug, Clone, Copy)] 25 | pub struct Pgsql<'a> { 26 | pub project_id: RwSignal, 27 | user: Option<&'a str>, 28 | password: Option<&'a str>, 29 | database: Option<&'a str>, 30 | host: Option<&'a str>, 31 | port: Option<&'a str>, 32 | pub status: RwSignal, 33 | pub schemas: RwSignal>, 34 | pub tables: RwSignal>>, 35 | } 36 | 37 | impl<'a> Pgsql<'a> { 38 | pub fn new(project_id: String) -> Self { 39 | Self { 40 | project_id: RwSignal::new(project_id), 41 | status: RwSignal::default(), 42 | schemas: RwSignal::default(), 43 | tables: RwSignal::default(), 44 | user: None, 45 | password: None, 46 | database: None, 47 | host: None, 48 | port: None, 49 | } 50 | } 51 | 52 | pub async fn connector(&self) -> Result { 53 | self 54 | .status 55 | .update(|prev| *prev = ProjectConnectionStatus::Connecting); 56 | let status = invoke::( 57 | Invoke::PgsqlConnector.as_ref(), 58 | &InvokePgsqlConnectorArgs { 59 | project_id: &self.project_id.get(), 60 | key: Some([ 61 | self.user.unwrap(), 62 | self.password.unwrap(), 63 | self.database.unwrap(), 64 | self.host.unwrap(), 65 | self.port.unwrap(), 66 | ]), 67 | }, 68 | ) 69 | .await; 70 | if status == ProjectConnectionStatus::Connected { 71 | self.load_schemas().await; 72 | } 73 | self.status.update(|prev| *prev = status.clone()); 74 | Ok(status) 75 | } 76 | 77 | pub async fn load_schemas(&self) { 78 | let schemas = invoke::( 79 | Invoke::PgsqlLoadSchemas.as_ref(), 80 | &InvokePgsqlLoadSchemasArgs { 81 | project_id: &self.project_id.get(), 82 | }, 83 | ) 84 | .await; 85 | self.schemas.set(schemas); 86 | } 87 | 88 | pub async fn load_tables(&self, schema: &str) { 89 | if self.tables.get().contains_key(schema) { 90 | return; 91 | } 92 | let tables = invoke::( 93 | Invoke::PgsqlLoadTables.as_ref(), 94 | &InvokePgsqlLoadTablesArgs { 95 | project_id: &self.project_id.get(), 96 | schema, 97 | }, 98 | ) 99 | .await; 100 | self.tables.update(|prev| { 101 | prev.insert(schema.to_owned(), tables); 102 | }); 103 | } 104 | 105 | #[set_running_query] 106 | pub async fn run_default_table_query(&self, sql: &str) { 107 | let tabs_store = expect_context::(); 108 | 109 | let selected_projects = tabs_store.selected_projects.get(); 110 | let project_id = selected_projects.get(tabs_store.convert_selected_tab_to_index()); 111 | 112 | if !selected_projects.is_empty() 113 | && project_id.is_some_and(|id| id.as_str() != self.project_id.get()) 114 | { 115 | tabs_store.add_tab(&self.project_id.get()); 116 | } 117 | 118 | tabs_store.set_editor_value(sql); 119 | tabs_store.selected_projects.update(|prev| { 120 | let index = tabs_store.convert_selected_tab_to_index(); 121 | match prev.get_mut(index) { 122 | Some(project) => project.clone_from(&self.project_id.get()), 123 | None => prev.push(self.project_id.get().clone()), 124 | } 125 | }); 126 | 127 | let query = invoke::( 128 | Invoke::PgsqlRunQuery.as_ref(), 129 | &InvokePgsqlRunQueryArgs { 130 | project_id: &self.project_id.get(), 131 | sql, 132 | }, 133 | ) 134 | .await; 135 | let (cols, rows, query_time) = query; 136 | tabs_store.sql_results.update(|prev| { 137 | let index = tabs_store.convert_selected_tab_to_index(); 138 | match prev.get_mut(index) { 139 | Some(sql_result) => *sql_result = (cols, rows), 140 | None => prev.push((cols, rows)), 141 | } 142 | }); 143 | let qp_store = expect_context::(); 144 | qp_store.update(|prev| { 145 | let new = QueryPerformanceAtom::new(prev.len(), sql, query_time); 146 | prev.push_front(new); 147 | }); 148 | } 149 | 150 | pub fn select_tables_by_schema(&self, schema: &str) -> Option> { 151 | self.tables.get().get(schema).cloned() 152 | } 153 | 154 | pub fn load_connection_details( 155 | &mut self, 156 | user: &'a str, 157 | password: &'a str, 158 | database: &'a str, 159 | host: &'a str, 160 | port: &'a str, 161 | ) { 162 | self.user = Some(user); 163 | self.password = Some(password); 164 | self.database = Some(database); 165 | self.host = Some(host); 166 | self.port = Some(port); 167 | } 168 | 169 | pub fn edit_connection_details(&self) { 170 | let atom = expect_context::(); 171 | atom.update(|prev| { 172 | prev.project_id = self.project_id.get().clone(); 173 | prev.user = self.user.unwrap().to_string(); 174 | prev.password = self.password.unwrap().to_string(); 175 | prev.database = self.database.unwrap().to_string(); 176 | prev.host = self.host.unwrap().to_string(); 177 | prev.port = self.port.unwrap().to_string(); 178 | }); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/databases/pgsql/index.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use leptos::*; 4 | use leptos_icons::*; 5 | use leptos_toaster::{Toast, ToastId, ToastVariant, Toasts}; 6 | 7 | use super::{driver::Pgsql, schema::Schema}; 8 | use crate::{ 9 | modals::add_pgsql_connection::AddPgsqlConnection, 10 | store::{projects::ProjectsStore, tabs::TabsStore}, 11 | }; 12 | use common::enums::ProjectConnectionStatus; 13 | 14 | #[component] 15 | pub fn Pgsql(project_id: String) -> impl IntoView { 16 | let project_id = Rc::new(project_id); 17 | let tabs_store = expect_context::(); 18 | let projects_store = expect_context::(); 19 | let project_details = projects_store.select_project_by_name(&project_id).unwrap(); 20 | let project_details = Box::leak(Box::new(project_details)); 21 | // [user, password, host, port] 22 | let mut pgsql = Pgsql::new(project_id.clone().to_string()); 23 | { 24 | pgsql.load_connection_details( 25 | &project_details[1], 26 | &project_details[2], 27 | &project_details[3], 28 | &project_details[4], 29 | &project_details[5], 30 | ); 31 | } 32 | let show = create_rw_signal(false); 33 | let toast_context = expect_context::(); 34 | let create_toast = move |variant: ToastVariant, title: String| { 35 | let toast_id = ToastId::new(); 36 | toast_context.toast( 37 | view! { }, 38 | Some(toast_id), 39 | None, // options 40 | ); 41 | }; 42 | 43 | let connect = create_action(move |pgsql: &Pgsql| { 44 | let pgsql = *pgsql; 45 | async move { 46 | let status = pgsql.connector().await.unwrap(); 47 | match status { 48 | ProjectConnectionStatus::Connected => { 49 | create_toast(ToastVariant::Success, "Connected to project".into()); 50 | } 51 | ProjectConnectionStatus::Failed => { 52 | create_toast(ToastVariant::Error, "Failed to connect to project".into()) 53 | } 54 | _ => create_toast(ToastVariant::Warning, "Failed to connect to project".into()), 55 | } 56 | } 57 | }); 58 | let delete_project = create_action( 59 | move |(projects_store, project_id): &(ProjectsStore, String)| { 60 | let projects_store = *projects_store; 61 | let project_id = project_id.clone(); 62 | async move { 63 | projects_store.delete_project(&project_id).await; 64 | } 65 | }, 66 | ); 67 | let connect = move || { 68 | if pgsql.status.get() == ProjectConnectionStatus::Connected { 69 | return; 70 | } 71 | connect.dispatch(pgsql); 72 | }; 73 | 74 | view! { 75 | 76 | 77 |
78 |
79 | 114 |
115 | 128 | 138 | 151 |
152 |
153 |
154 | 155 | } 160 | } 161 | /> 162 | 163 | 164 |
165 |
166 |
167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /src/databases/pgsql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod driver; 2 | pub mod index; 3 | pub mod schema; 4 | pub mod table; 5 | 6 | -------------------------------------------------------------------------------- /src/databases/pgsql/schema.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use leptos::*; 4 | use leptos_icons::*; 5 | 6 | use super::{driver::Pgsql, table::Table}; 7 | 8 | #[component] 9 | pub fn Schema(schema: String) -> impl IntoView { 10 | let (show, set_show) = create_signal(false); 11 | let (is_loading, set_is_loading) = create_signal(false); 12 | let schema = Rc::new(schema); 13 | let pgsql = expect_context::(); 14 | let load_tables = create_action(move |schema: &String| { 15 | let schema = schema.clone(); 16 | async move { 17 | pgsql.load_tables(&schema).await; 18 | set_is_loading(false); 19 | set_show(!show()); 20 | } 21 | }); 22 | 23 | view! { 24 |
25 | 48 |
49 | 50 | } 64 | } 65 | } 66 | /> 67 | 68 | 69 |
70 |
71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/databases/pgsql/table.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use leptos::*; 4 | use leptos_icons::*; 5 | 6 | use crate::databases::pgsql::driver::Pgsql; 7 | 8 | #[component] 9 | pub fn Table(table: (String, String), schema: String) -> impl IntoView { 10 | let table = Rc::new(table); 11 | let schema = Rc::new(schema); 12 | let pgsql = expect_context::(); 13 | let query = create_action(move |(schema, table, pgsql): &(String, String, Pgsql)| { 14 | let pgsql = *pgsql; 15 | let schema = schema.clone(); 16 | let table = table.clone(); 17 | 18 | async move { 19 | pgsql 20 | .run_default_table_query(&format!("SELECT * FROM {}.{} LIMIT 100;", schema, table)) 21 | .await; 22 | } 23 | }); 24 | 25 | view! { 26 |
33 | 34 |
35 | 36 |

{table.0.to_string()}

37 |
38 |

{table.1.to_string()}

39 |
40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/enums.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, PartialEq, Eq)] 2 | pub enum QueryTableLayout { 3 | Grid, 4 | Records, 5 | } 6 | -------------------------------------------------------------------------------- /src/footer.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_icons::*; 3 | 4 | use crate::enums::QueryTableLayout; 5 | 6 | #[component] 7 | pub fn Footer() -> impl IntoView { 8 | let table_view = expect_context::>(); 9 | 10 | view! { 11 |
12 | 20 | 27 |
28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/grid_view.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | 3 | use crate::store::tabs::TabsStore; 4 | 5 | #[component] 6 | pub fn GridView() -> impl IntoView { 7 | let tabs_store = expect_context::(); 8 | 9 | view! { 10 | 11 | 12 | 13 | {col} } 18 | } 19 | /> 20 | 21 | 22 | 23 | 24 | 30 | {cell} } 35 | } 36 | /> 37 | 38 | 39 | } 40 | } 41 | /> 42 | 43 | 44 |
45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/invoke.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | pub enum Invoke { 6 | ProjectDbSelect, 7 | ProjectDbInsert, 8 | ProjectDbDelete, 9 | 10 | QueryDbSelect, 11 | QueryDbInsert, 12 | QueryDbDelete, 13 | 14 | PgsqlConnector, 15 | PgsqlLoadSchemas, 16 | PgsqlLoadTables, 17 | #[allow(dead_code)] 18 | PgsqlLoadRelations, 19 | PgsqlRunQuery, 20 | } 21 | 22 | impl Display for Invoke { 23 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 24 | match self { 25 | Invoke::ProjectDbSelect => write!(f, "project_db_select"), 26 | Invoke::ProjectDbInsert => write!(f, "project_db_insert"), 27 | Invoke::ProjectDbDelete => write!(f, "project_db_delete"), 28 | 29 | Invoke::QueryDbSelect => write!(f, "query_db_select"), 30 | Invoke::QueryDbInsert => write!(f, "query_db_insert"), 31 | Invoke::QueryDbDelete => write!(f, "query_db_delete"), 32 | 33 | Invoke::PgsqlConnector => write!(f, "pgsql_connector"), 34 | Invoke::PgsqlLoadRelations => write!(f, "pgsql_load_relations"), 35 | Invoke::PgsqlLoadTables => write!(f, "pgsql_load_tables"), 36 | Invoke::PgsqlLoadSchemas => write!(f, "pgsql_load_schemas"), 37 | Invoke::PgsqlRunQuery => write!(f, "pgsql_run_query"), 38 | } 39 | } 40 | } 41 | 42 | impl AsRef for Invoke { 43 | fn as_ref(&self) -> &str { 44 | match *self { 45 | Invoke::ProjectDbSelect => "project_db_select", 46 | Invoke::ProjectDbInsert => "project_db_insert", 47 | Invoke::ProjectDbDelete => "project_db_delete", 48 | 49 | Invoke::QueryDbSelect => "query_db_select", 50 | Invoke::QueryDbInsert => "query_db_insert", 51 | Invoke::QueryDbDelete => "query_db_delete", 52 | 53 | Invoke::PgsqlConnector => "pgsql_connector", 54 | Invoke::PgsqlLoadSchemas => "pgsql_load_schemas", 55 | Invoke::PgsqlLoadTables => "pgsql_load_tables", 56 | Invoke::PgsqlLoadRelations => "pgsql_load_relations", 57 | Invoke::PgsqlRunQuery => "pgsql_run_query", 58 | } 59 | } 60 | } 61 | 62 | #[derive(Serialize, Deserialize)] 63 | pub struct InvokePgsqlConnectorArgs<'a> { 64 | pub project_id: &'a str, 65 | pub key: Option<[&'a str; 5]>, 66 | } 67 | 68 | #[derive(Serialize, Deserialize)] 69 | pub struct InvokePgsqlLoadSchemasArgs<'a> { 70 | pub project_id: &'a str, 71 | } 72 | 73 | #[derive(Serialize, Deserialize)] 74 | pub struct InvokePgsqlLoadTablesArgs<'a> { 75 | pub project_id: &'a str, 76 | pub schema: &'a str, 77 | } 78 | 79 | #[derive(Serialize, Deserialize)] 80 | pub struct InvokePgsqlRunQueryArgs<'a> { 81 | pub project_id: &'a str, 82 | pub sql: &'a str, 83 | } 84 | 85 | #[derive(Serialize, Deserialize)] 86 | pub struct InvokeProjectDbInsertArgs<'a> { 87 | pub project_id: &'a str, 88 | pub project_details: Vec, 89 | } 90 | 91 | #[derive(Serialize, Deserialize)] 92 | pub struct InvokeProjectDbDeleteArgs<'a> { 93 | pub project_id: &'a str, 94 | } 95 | 96 | #[derive(Serialize, Deserialize)] 97 | pub struct InvokeQueryDbInsertArgs<'a> { 98 | pub query_id: &'a str, 99 | pub sql: &'a str, 100 | } 101 | 102 | #[derive(Serialize, Deserialize)] 103 | pub struct InvokeQueryDbDeleteArgs<'a> { 104 | pub query_id: &'a str, 105 | } 106 | 107 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(pattern)] 2 | 3 | mod app; 4 | mod dashboard; 5 | mod databases; 6 | mod enums; 7 | mod footer; 8 | mod grid_view; 9 | mod invoke; 10 | mod modals; 11 | mod performane; 12 | mod record_view; 13 | mod sidebar; 14 | mod store; 15 | 16 | use app::App; 17 | use leptos::*; 18 | 19 | fn main() { 20 | leptos_devtools::devtools(); 21 | mount_to_body(|| view! { }) 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/modals/add_custom_query.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use common::enums::Drivers; 4 | use leptos::*; 5 | use thaw::{Modal, ModalFooter}; 6 | 7 | use crate::store::queries::QueriesStore; 8 | 9 | #[component] 10 | pub fn AddCustomQuery( 11 | show: RwSignal, 12 | project_id: String, 13 | driver: Drivers, 14 | database: String, 15 | ) -> impl IntoView { 16 | let project_id = Rc::new(project_id); 17 | let project_id_clone = project_id.clone(); 18 | let query_store = expect_context::(); 19 | let (title, set_title) = create_signal(String::new()); 20 | let insert_query = create_action( 21 | move |(query_db, project_id, title, driver, database): &( 22 | QueriesStore, 23 | String, 24 | String, 25 | Drivers, 26 | String, 27 | )| { 28 | let query_db_clone = *query_db; 29 | let project_id = project_id.clone(); 30 | let title = title.clone(); 31 | let driver = *driver; 32 | let database = database.clone(); 33 | async move { 34 | query_db_clone 35 | .insert_query(&project_id, &title, &driver, &database) 36 | .await; 37 | } 38 | }, 39 | ); 40 | 41 | view! { 42 | 43 |
44 |

Project: {&*project_id_clone}

45 | 52 |
53 | 54 | 55 |
56 | 77 | 83 |
84 |
85 |
86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /src/modals/add_pgsql_connection.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use thaw::{Modal, ModalFooter}; 3 | 4 | use crate::store::{atoms::PgsqlConnectionDetailsContext, projects::ProjectsStore}; 5 | 6 | #[component] 7 | pub fn AddPgsqlConnection(show: RwSignal) -> impl IntoView { 8 | let projects_store = expect_context::(); 9 | let params = expect_context::(); 10 | let save_project = create_action( 11 | move |(project_id, project_details): &(String, Vec)| { 12 | let project_id = project_id.clone(); 13 | let project_details = project_details.clone(); 14 | async move { 15 | projects_store 16 | .insert_project(&project_id, project_details) 17 | .await; 18 | show.set(false); 19 | } 20 | }, 21 | ); 22 | 23 | view! { 24 | 25 |
26 | 35 | 36 | 43 | 44 | 51 | 52 | 59 | 60 | 67 | 68 | 75 |
76 | 77 | 78 |
79 | 100 | 107 |
108 |
109 |
110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/modals/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod add_custom_query; 2 | pub mod add_pgsql_connection; 3 | 4 | -------------------------------------------------------------------------------- /src/performane.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | 3 | use crate::store::atoms::QueryPerformanceContext; 4 | 5 | #[component] 6 | pub fn Performance() -> impl IntoView { 7 | let performance = expect_context::(); 8 | 9 | view! { 10 |
11 |

"Performance"

12 |
13 | 19 | {item.message.clone()} 20 |
21 | } 22 | } 23 | /> 24 | 25 |
26 | 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/record_view.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | 3 | use crate::store::tabs::TabsStore; 4 | 5 | #[component] 6 | pub fn RecordView() -> impl IntoView { 7 | let tabs_store = expect_context::(); 8 | let columns = tabs_store.select_active_editor_sql_result().unwrap().0; 9 | let first_row = tabs_store 10 | .select_active_editor_sql_result() 11 | .unwrap() 12 | .1 13 | .first() 14 | .unwrap() 15 | .clone(); 16 | let columns_with_values = columns.into_iter().zip(first_row).collect::>(); 17 | 18 | view! { 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | } 37 | } 38 | /> 39 | 40 | 41 |
"Properties""Values"
{col}{val}
42 | } 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/sidebar/index.rs: -------------------------------------------------------------------------------- 1 | use leptos::*; 2 | use leptos_use::{use_document, use_event_listener}; 3 | 4 | use crate::{ 5 | databases::pgsql::index::Pgsql, 6 | modals::add_pgsql_connection::AddPgsqlConnection, 7 | store::{projects::ProjectsStore, queries::QueriesStore}, 8 | }; 9 | use common::enums::Drivers; 10 | 11 | use super::queries::Queries; 12 | 13 | #[component] 14 | pub fn Sidebar() -> impl IntoView { 15 | let projects_store = expect_context::(); 16 | let queries_store = expect_context::(); 17 | let show = create_rw_signal(false); 18 | let _ = use_event_listener(use_document(), ev::keydown, move |event| { 19 | if event.key() == "Escape" { 20 | show.set(false); 21 | } 22 | }); 23 | let _ = create_resource( 24 | || {}, 25 | move |_| async move { 26 | projects_store.load_projects().await; 27 | queries_store.load_queries().await; 28 | }, 29 | ); 30 | 31 | view! { 32 |
33 | 34 |
35 |
36 |

Projects

37 | 43 |
44 | 51 | 52 |
53 | } 54 | } else { 55 | view! {
} 56 | } 57 | } 58 | /> 59 | 60 |
61 | }> 62 |
63 |

Saved Queries

64 |
65 | 66 |
67 |
68 |
69 | 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/sidebar/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod index; 2 | pub mod queries; 3 | pub mod query; 4 | 5 | -------------------------------------------------------------------------------- /src/sidebar/queries.rs: -------------------------------------------------------------------------------- 1 | use crate::store::queries::QueriesStore; 2 | use leptos::*; 3 | 4 | use super::query::Query; 5 | 6 | #[component] 7 | pub fn Queries() -> impl IntoView { 8 | let queries_store = expect_context::(); 9 | let _ = create_resource( 10 | move || queries_store.get(), 11 | move |_| async move { 12 | queries_store.load_queries().await; 13 | }, 14 | ); 15 | 16 | view! { 17 | } 21 | /> 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/sidebar/query.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use leptos::*; 4 | use leptos_icons::*; 5 | 6 | use crate::store::{queries::QueriesStore, tabs::TabsStore}; 7 | 8 | #[component] 9 | pub fn Query(query_id: String, sql: String) -> impl IntoView { 10 | let query_id = Arc::new(query_id); 11 | let sql = Arc::new(sql); 12 | let query_store = expect_context::(); 13 | let tabs_store = expect_context::(); 14 | 15 | view! { 16 |
17 | 37 | 52 |
53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/store/atoms.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use common::enums::Drivers; 4 | use leptos::RwSignal; 5 | use rsql_proc_macros::StructIntoIterator; 6 | 7 | #[derive(Debug, Default, Clone)] 8 | pub struct QueryPerformanceAtom { 9 | pub id: usize, 10 | pub message: String, 11 | } 12 | 13 | impl QueryPerformanceAtom { 14 | pub fn new(id: usize, sql: &str, query_time: f32) -> Self { 15 | Self { 16 | id, 17 | message: format!( 18 | "[{}]: {} is executed in {} ms", 19 | chrono::Utc::now(), 20 | sql, 21 | query_time 22 | ), 23 | } 24 | } 25 | } 26 | 27 | pub type QueryPerformanceContext = RwSignal>; 28 | 29 | #[derive(Debug, Default, Clone)] 30 | pub struct RunQueryAtom { 31 | pub is_running: bool, 32 | } 33 | 34 | pub type RunQueryContext = RwSignal; 35 | 36 | #[derive(Clone, StructIntoIterator)] 37 | pub struct PgsqlConnectionDetailsAtom { 38 | pub project_id: String, 39 | pub driver: Drivers, 40 | pub user: String, 41 | pub password: String, 42 | pub database: String, 43 | pub host: String, 44 | pub port: String, 45 | } 46 | 47 | impl Default for PgsqlConnectionDetailsAtom { 48 | fn default() -> Self { 49 | Self { 50 | project_id: String::new(), 51 | driver: Drivers::PGSQL, 52 | user: String::new(), 53 | password: String::new(), 54 | database: String::new(), 55 | host: String::new(), 56 | port: String::new(), 57 | } 58 | } 59 | } 60 | 61 | pub type PgsqlConnectionDetailsContext = RwSignal; 62 | -------------------------------------------------------------------------------- /src/store/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod atoms; 2 | pub mod projects; 3 | pub mod queries; 4 | pub mod tabs; 5 | 6 | -------------------------------------------------------------------------------- /src/store/projects.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeMap, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use common::{enums::Drivers, types::BTreeVecStore}; 7 | use leptos::{RwSignal, SignalGet, SignalSet}; 8 | use tauri_sys::core::invoke; 9 | 10 | use crate::invoke::{Invoke, InvokeProjectDbDeleteArgs, InvokeProjectDbInsertArgs}; 11 | 12 | #[derive(Clone, Copy, Debug)] 13 | pub struct ProjectsStore(pub RwSignal); 14 | 15 | impl Deref for ProjectsStore { 16 | type Target = RwSignal; 17 | 18 | fn deref(&self) -> &Self::Target { 19 | &self.0 20 | } 21 | } 22 | 23 | impl DerefMut for ProjectsStore { 24 | fn deref_mut(&mut self) -> &mut Self::Target { 25 | &mut self.0 26 | } 27 | } 28 | 29 | impl ProjectsStore { 30 | #[must_use] 31 | pub fn new() -> Self { 32 | Self(RwSignal::default()) 33 | } 34 | 35 | pub fn select_project_by_name(&self, project_id: &str) -> Option> { 36 | self.get().get(project_id).cloned() 37 | } 38 | 39 | pub fn select_driver_by_project(&self, project_id: Option<&str>) -> Drivers { 40 | if project_id.is_none() { 41 | return Drivers::PGSQL; 42 | } 43 | 44 | let project = self.select_project_by_name(project_id.unwrap()).unwrap(); 45 | let driver = project.first().unwrap(); 46 | 47 | match driver.as_str() { 48 | "PGSQL" => Drivers::PGSQL, 49 | _ => unreachable!(), 50 | } 51 | } 52 | 53 | pub fn select_database_by_project(&self, project_id: Option<&str>) -> String { 54 | if project_id.is_none() { 55 | return String::new(); 56 | } 57 | 58 | let project = self.select_project_by_name(project_id.unwrap()).unwrap(); 59 | project.get(3).unwrap().clone() 60 | } 61 | 62 | pub async fn load_projects(&self) { 63 | let projects = 64 | invoke::>>(Invoke::ProjectDbSelect.as_ref(), &()).await; 65 | self.set(projects); 66 | } 67 | 68 | pub async fn insert_project(&self, project_id: &str, project_details: Vec) { 69 | let _ = invoke::<()>( 70 | Invoke::ProjectDbInsert.as_ref(), 71 | &InvokeProjectDbInsertArgs { 72 | project_id, 73 | project_details, 74 | }, 75 | ) 76 | .await; 77 | self.load_projects().await; 78 | } 79 | 80 | pub async fn delete_project(&self, project_id: &str) { 81 | let _ = invoke::<()>( 82 | Invoke::ProjectDbDelete.as_ref(), 83 | &InvokeProjectDbDeleteArgs { project_id }, 84 | ) 85 | .await; 86 | self.load_projects().await; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/store/queries.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeMap, 3 | ops::{Deref, DerefMut}, 4 | }; 5 | 6 | use common::{enums::Drivers, types::BTreeStore}; 7 | use leptos::*; 8 | use tauri_sys::core::invoke; 9 | 10 | use crate::invoke::{Invoke, InvokeQueryDbDeleteArgs, InvokeQueryDbInsertArgs}; 11 | 12 | use super::tabs::TabsStore; 13 | 14 | #[derive(Clone, Copy, Debug)] 15 | pub struct QueriesStore(pub RwSignal); 16 | 17 | impl Deref for QueriesStore { 18 | type Target = RwSignal; 19 | 20 | fn deref(&self) -> &Self::Target { 21 | &self.0 22 | } 23 | } 24 | 25 | impl DerefMut for QueriesStore { 26 | fn deref_mut(&mut self) -> &mut Self::Target { 27 | &mut self.0 28 | } 29 | } 30 | 31 | impl QueriesStore { 32 | #[must_use] 33 | pub fn new() -> Self { 34 | Self(RwSignal::default()) 35 | } 36 | 37 | pub async fn load_queries(&self) { 38 | let saved_queries = 39 | invoke::>(Invoke::QueryDbSelect.as_ref(), &()).await; 40 | self.update(|prev| { 41 | *prev = saved_queries.into_iter().collect(); 42 | }); 43 | } 44 | 45 | pub async fn insert_query( 46 | &self, 47 | project_id: &str, 48 | title: &str, 49 | driver: &Drivers, 50 | database: &str, 51 | ) { 52 | let tabs_store = expect_context::(); 53 | let sql = tabs_store.select_active_editor_value(); 54 | let _ = invoke::<()>( 55 | Invoke::QueryDbInsert.as_ref(), 56 | &InvokeQueryDbInsertArgs { 57 | query_id: &format!("{}:{}:{}:{}", project_id, database, driver, title), 58 | sql: &sql, 59 | }, 60 | ) 61 | .await; 62 | self.load_queries().await; 63 | } 64 | 65 | pub async fn delete_query(&self, query_id: &str) { 66 | let _ = invoke::<()>( 67 | Invoke::QueryDbDelete.as_ref(), 68 | &InvokeQueryDbDeleteArgs { query_id }, 69 | ) 70 | .await; 71 | self.load_queries().await; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/store/tabs.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use common::enums::ProjectConnectionStatus; 4 | use leptos::{create_rw_signal, expect_context, RwSignal, SignalGet, SignalSet, SignalUpdate}; 5 | use monaco::api::CodeEditor; 6 | use rsql_proc_macros::set_running_query; 7 | use tauri_sys::core::invoke; 8 | 9 | use crate::{ 10 | dashboard::query_editor::ModelCell, 11 | invoke::{Invoke, InvokePgsqlConnectorArgs, InvokePgsqlRunQueryArgs}, 12 | }; 13 | 14 | use super::atoms::{QueryPerformanceAtom, QueryPerformanceContext, RunQueryAtom, RunQueryContext}; 15 | 16 | struct QueryInfo { 17 | query: String, 18 | _start_line: f64, 19 | _end_line: f64, 20 | } 21 | 22 | #[derive(Copy, Clone, Debug)] 23 | pub struct TabsStore { 24 | pub selected_tab: RwSignal, 25 | pub active_tabs: RwSignal, 26 | pub editors: RwSignal>, 27 | #[allow(clippy::type_complexity)] 28 | pub sql_results: RwSignal, Vec>)>>, 29 | pub selected_projects: RwSignal>, 30 | } 31 | 32 | unsafe impl Send for TabsStore {} 33 | unsafe impl Sync for TabsStore {} 34 | 35 | impl Default for TabsStore { 36 | fn default() -> Self { 37 | Self::new() 38 | } 39 | } 40 | 41 | impl TabsStore { 42 | #[must_use] 43 | pub fn new() -> Self { 44 | Self { 45 | selected_tab: create_rw_signal(String::from("0")), 46 | active_tabs: create_rw_signal(1), 47 | editors: create_rw_signal(Vec::new()), 48 | sql_results: create_rw_signal(Vec::new()), 49 | selected_projects: create_rw_signal(Vec::new()), 50 | } 51 | } 52 | 53 | #[set_running_query] 54 | pub async fn run_query(&self) { 55 | let project_ids = self.selected_projects.get(); 56 | let project_id = project_ids 57 | .get(self.convert_selected_tab_to_index()) 58 | .unwrap(); 59 | let active_editor = self.select_active_editor(); 60 | let position = active_editor 61 | .borrow() 62 | .as_ref() 63 | .unwrap() 64 | .as_ref() 65 | .get_position() 66 | .unwrap(); 67 | let sql = self.select_active_editor_value(); 68 | let sql = self 69 | .find_query_for_line(&sql, position.line_number()) 70 | .unwrap(); 71 | let (cols, rows, query_time) = invoke::<(Vec, Vec>, f32)>( 72 | Invoke::PgsqlRunQuery.as_ref(), 73 | &InvokePgsqlRunQueryArgs { 74 | project_id, 75 | sql: &sql.query, 76 | }, 77 | ) 78 | .await; 79 | self.sql_results.update(|prev| { 80 | let index = self.convert_selected_tab_to_index(); 81 | match prev.get_mut(index) { 82 | Some(sql_result) => *sql_result = (cols, rows), 83 | None => prev.push((cols, rows)), 84 | } 85 | }); 86 | let qp_store = expect_context::(); 87 | qp_store.update(|prev| { 88 | let new = QueryPerformanceAtom::new(prev.len(), &sql.query, query_time); 89 | prev.push_front(new); 90 | }); 91 | } 92 | 93 | // TODO: Need to be more generic if we want to support other databases 94 | pub async fn load_query(&self, query_id: &str, sql: &str) { 95 | let splitted_key = query_id.split(':').collect::>(); 96 | let selected_projects = self.selected_projects.get(); 97 | let project_id = selected_projects.get(self.convert_selected_tab_to_index()); 98 | if !self.selected_projects.get().is_empty() 99 | && project_id.is_some_and(|id| id.as_str() != splitted_key[0]) 100 | { 101 | self.add_tab(splitted_key[0]); 102 | } 103 | self.set_editor_value(sql); 104 | self.selected_projects.update(|prev| { 105 | let index = self.convert_selected_tab_to_index(); 106 | match prev.get_mut(index) { 107 | Some(project) => *project = splitted_key[0].to_string(), 108 | None => prev.push(splitted_key[0].to_string()), 109 | } 110 | }); 111 | let _ = invoke::( 112 | Invoke::PgsqlConnector.as_ref(), 113 | &InvokePgsqlConnectorArgs { 114 | project_id: splitted_key[0], 115 | key: None, 116 | }, 117 | ) 118 | .await; 119 | self.run_query().await; 120 | } 121 | 122 | pub fn add_editor(&mut self, editor: Rc>>) { 123 | self.editors.update(|prev| { 124 | prev.push(editor); 125 | }); 126 | } 127 | 128 | pub fn add_tab(&self, project_id: &str) { 129 | if self.editors.get().len() == 1 && self.selected_projects.get().is_empty() { 130 | self.selected_projects.update(|prev| { 131 | prev.push(project_id.to_string()); 132 | }); 133 | return; 134 | } 135 | 136 | self.active_tabs.update(|prev| { 137 | *prev += 1; 138 | }); 139 | 140 | self.selected_tab.update(|prev| { 141 | *prev = (self.active_tabs.get() - 1).to_string(); 142 | }); 143 | 144 | self.selected_projects.update(|prev| { 145 | prev.push(project_id.to_string()); 146 | }); 147 | } 148 | 149 | pub fn close_tab(&self, index: usize) { 150 | if self.active_tabs.get() == 1 { 151 | return; 152 | } 153 | 154 | self.selected_tab.update(|prev| { 155 | *prev = (index - 1).to_string(); 156 | }); 157 | 158 | self.active_tabs.update(|prev| { 159 | *prev -= 1; 160 | }); 161 | 162 | self.editors.update(|prev| { 163 | prev.remove(index); 164 | }); 165 | } 166 | 167 | pub fn select_active_editor_sql_result(&self) -> Option<(Vec, Vec>)> { 168 | self 169 | .sql_results 170 | .get() 171 | .get(self.convert_selected_tab_to_index()) 172 | .cloned() 173 | } 174 | 175 | pub fn select_active_editor(&self) -> ModelCell { 176 | self 177 | .editors 178 | .get() 179 | .get(self.convert_selected_tab_to_index()) 180 | .unwrap() 181 | .clone() 182 | } 183 | 184 | pub fn select_active_editor_value(&self) -> String { 185 | self 186 | .editors 187 | .get() 188 | .get(self.convert_selected_tab_to_index()) 189 | .unwrap() 190 | .borrow() 191 | .as_ref() 192 | .unwrap() 193 | .get_model() 194 | .unwrap() 195 | .get_value() 196 | } 197 | 198 | pub fn set_editor_value(&self, value: &str) { 199 | self 200 | .editors 201 | .get() 202 | .get(self.convert_selected_tab_to_index()) 203 | .unwrap() 204 | .borrow() 205 | .as_ref() 206 | .unwrap() 207 | .get_model() 208 | .unwrap() 209 | .set_value(value); 210 | } 211 | 212 | pub fn convert_selected_tab_to_index(&self) -> usize { 213 | self.selected_tab.get().parse::().unwrap() 214 | } 215 | 216 | pub(self) fn find_query_for_line(&self, queries: &str, line_number: f64) -> Option { 217 | let mut start_line = 1f64; 218 | let mut end_line = 1f64; 219 | let mut current_query = String::new(); 220 | 221 | for line in queries.lines() { 222 | if !current_query.is_empty() { 223 | current_query.push('\n'); 224 | } 225 | current_query.push_str(line); 226 | end_line += 1f64; 227 | 228 | if line.ends_with(';') { 229 | if line_number >= start_line && line_number < end_line { 230 | return Some(QueryInfo { 231 | query: current_query.clone(), 232 | _start_line: start_line, 233 | _end_line: end_line - 1f64, 234 | }); 235 | } 236 | start_line = end_line; 237 | current_query.clear(); 238 | } 239 | } 240 | 241 | None 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: { 4 | files: ["*.html", "./src/**/*.rs"], 5 | }, 6 | theme: { 7 | extend: { 8 | borderWidth: { 9 | 1: "1px", 10 | }, 11 | }, 12 | }, 13 | plugins: [], 14 | }; 15 | --------------------------------------------------------------------------------