├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .lintstagedrc.json ├── .mocharc.yaml ├── .npmrc ├── .prettierignore ├── .prettierrc.json ├── .storybook ├── main.js └── preview.js ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.js ├── build ├── app.png ├── background.png ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── install-spinner.gif └── notarize.js ├── docker-compose.yml ├── docs ├── app │ ├── configuration-file.md │ ├── keyboard-shortcuts.md │ └── logging.md └── development │ ├── README.md │ ├── build-instructions.md │ ├── docker-databases.md │ ├── release.md │ ├── run-from-source.md │ └── test-core-changes.md ├── empty-shim.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── scripts ├── .eslintrc ├── dev-auto-restart.js ├── link-sqlectron-db-core.sh └── start-test-integration-deps.sh ├── src ├── browser │ ├── .eslintrc.json │ ├── app.ts │ ├── browser.ts │ ├── config.ts │ ├── core │ │ ├── config.ts │ │ ├── crypto.ts │ │ ├── db.ts │ │ ├── index.ts │ │ ├── limit.ts │ │ ├── servers.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ └── wait.ts │ │ └── validators │ │ │ └── server.ts │ ├── ipcMain.ts │ ├── logger.ts │ ├── main.ts │ ├── menu │ │ ├── darwin.ts │ │ ├── index.ts │ │ ├── linux.ts │ │ └── win32.ts │ ├── preload.ts │ ├── update-checker.ts │ └── window.ts ├── common │ ├── event.ts │ ├── types │ │ ├── api.ts │ │ ├── config.ts │ │ ├── database.ts │ │ ├── menu.ts │ │ └── server.ts │ └── utils │ │ ├── convert.ts │ │ └── string.ts ├── index.d.ts └── renderer │ ├── actions │ ├── columns.ts │ ├── config.ts │ ├── connections.ts │ ├── databases.ts │ ├── indexes.ts │ ├── keys.ts │ ├── queries.ts │ ├── routines.ts │ ├── schemas.ts │ ├── servers.ts │ ├── sqlscripts.ts │ ├── tables.ts │ ├── triggers.ts │ └── views.ts │ ├── api.ts │ ├── assets │ └── server-db-client │ │ ├── cassandra.png │ │ ├── mariadb.png │ │ ├── mysql.png │ │ ├── postgresql.png │ │ ├── redshift.png │ │ ├── sqlite.png │ │ └── sqlserver.png │ ├── components │ ├── _breakpoint.scss │ ├── checkbox.tsx │ ├── collapse-icon.tsx │ ├── confim-modal.tsx │ ├── database-diagram-modal.jsx │ ├── database-diagram-shapes.js │ ├── database-diagram.css │ ├── database-diagram.jsx │ ├── database-filter.tsx │ ├── database-item.tsx │ ├── database-list-item-metadata.tsx │ ├── database-list-item.tsx │ ├── database-list.tsx │ ├── footer.tsx │ ├── header.css │ ├── header.tsx │ ├── loader.tsx │ ├── log-status.tsx │ ├── logo-128px.png │ ├── message.tsx │ ├── override-ace.css │ ├── preview-modal.tsx │ ├── prompt-modal.tsx │ ├── query-result-table-cell.tsx │ ├── query-result-table.scss │ ├── query-result-table.tsx │ ├── query-result.tsx │ ├── query-results.tsx │ ├── query-tab.tsx │ ├── query-tabs.tsx │ ├── query.tsx │ ├── react-resizable.css │ ├── react-tabs.scss │ ├── require-context.ts │ ├── server-db-client-info-modal.tsx │ ├── server-filter.tsx │ ├── server-list-card.tsx │ ├── server-list-item.tsx │ ├── server-list.scss │ ├── server-list.tsx │ ├── server-modal-form.tsx │ ├── settings-modal-form.tsx │ ├── tab-list.tsx │ ├── table-submenu.tsx │ └── update-checker.tsx │ ├── containers │ ├── app.css │ ├── app.tsx │ ├── query-browser.css │ ├── query-browser.tsx │ ├── root.tsx │ ├── server-management.tsx │ └── sqlectron.gif │ ├── entry.tsx │ ├── hooks │ ├── index.ts │ ├── redux.ts │ ├── useEffectDebugger.ts │ └── usePrevious.ts │ ├── index.html │ ├── reducers │ ├── columns.ts │ ├── config.ts │ ├── connections.ts │ ├── databases.ts │ ├── index.ts │ ├── indexes.ts │ ├── keys.ts │ ├── queries.ts │ ├── routines.ts │ ├── schemas.ts │ ├── servers.ts │ ├── sqlscripts.ts │ ├── status.ts │ ├── tables.ts │ ├── triggers.ts │ └── views.ts │ ├── store │ └── configure.ts │ └── utils │ ├── config.ts │ ├── context-menu.ts │ ├── file.ts │ ├── menu.ts │ └── wait.ts ├── stories └── components │ └── checkbox.stories.tsx ├── test ├── browser │ ├── test.config.ts │ ├── test.servers.ts │ ├── test.utils.ts │ ├── utils-stub.ts │ └── validators │ │ └── test.server.ts ├── common │ └── utils │ │ └── test.string.ts ├── e2e │ ├── helper.ts │ ├── test.mainWindow.ts │ ├── test.sqlite.ts │ └── test.ssh-mysql.ts ├── fixtures │ ├── browser │ │ ├── sqlectron.json │ │ └── sqlectron.prepared.json │ ├── simple │ │ └── sqlectron-sample.json │ ├── sqlite │ │ └── sample-sqlectron.json │ └── ssh-mysql │ │ └── sample-sqlectron.json ├── renderer │ └── components │ │ └── test.checkbox.tsx └── setup.ts ├── tsconfig.build.json ├── tsconfig.json ├── vendor └── renderer │ ├── lato │ ├── fonts │ │ ├── Lato-Regular.eot │ │ ├── Lato-Regular.ttf │ │ ├── Lato-Regular.woff │ │ └── Lato-Regular.woff2 │ └── latofonts.css │ └── semantic-ui │ ├── README.md │ ├── semantic.css │ ├── semantic.js │ └── themes │ └── default │ └── assets │ ├── fonts │ ├── icons.eot │ ├── icons.otf │ ├── icons.svg │ ├── icons.ttf │ ├── icons.woff │ └── icons.woff2 │ └── images │ └── flags.png └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | releases 4 | installers 5 | npm-debug.log 6 | .DS_Store 7 | .tmp 8 | .git 9 | node_modules/ 10 | .tmp/ 11 | dist/ 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | insert_final_newline = true 3 | indent_style = space 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | vendor/ 4 | out/ 5 | storybook-static/ 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | # Binary files. 4 | *.gif binary 5 | *.png binary 6 | *.jpg binary 7 | *.woff binary 8 | *.woff2 binary 9 | *.eot binary 10 | *.otf binary 11 | *.ttf binary 12 | *.icns binary 13 | 14 | # Mark vendor to ignore by GH linguist 15 | vendor linguist-vendored 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ${{ matrix.os }} 9 | 10 | env: 11 | node-version: 14 12 | 13 | strategy: 14 | matrix: 15 | # TODO: upgrade to latest Ubuntu 16 | os: [ubuntu-18.04, macos-latest, windows-latest] 17 | 18 | steps: 19 | - name: Install Linux Dependencies 20 | if: runner.os == 'Linux' 21 | run: sudo apt-get install libgnome-keyring-dev icnsutils graphicsmagick rpm bsdtar 22 | 23 | - uses: actions/checkout@v3 24 | 25 | - name: Use Node.js ${{ env.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ env.node-version }} 29 | cache: npm 30 | 31 | # Version of npm that comes in 14.x is npm@6 32 | - run: npm i -g npm@7 33 | 34 | - run: npm ci 35 | 36 | - run: npm run lint 37 | 38 | - run: npm run format:check 39 | 40 | - run: npm run test 41 | 42 | - name: Prepare integration test dependencies 43 | if: runner.os == 'Linux' 44 | run: ./scripts/start-test-integration-deps.sh 45 | 46 | - name: Build app for e2e test 47 | if: runner.os == 'Linux' 48 | run: npm run compile 49 | 50 | - name: Run e2e test 51 | if: runner.os == 'Linux' 52 | run: xvfb-run --auto-servernum npm run test:e2e 53 | 54 | - run: npm run dist -- --publish never 55 | 56 | - name: Upload artifacts 57 | uses: actions/upload-artifact@v3 58 | with: 59 | name: ${{ matrix.os }} 60 | path: dist 61 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [ 'created' ] 6 | 7 | jobs: 8 | build: 9 | environment: deploy 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | env: 14 | node-version: 14 15 | 16 | strategy: 17 | matrix: 18 | # TODO: upgrade to latest Ubuntu 19 | os: [ubuntu-18.04, macos-latest, windows-latest] 20 | 21 | steps: 22 | - name: Install Linux Dependencies 23 | if: runner.os == 'Linux' 24 | run: | 25 | sudo apt-get update 26 | sudo apt-get install libgnome-keyring-dev icnsutils graphicsmagick rpm bsdtar gcc-multilib g++-multilib 27 | 28 | - uses: actions/checkout@v3 29 | 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | 35 | - run: npm ci 36 | 37 | - run: npm test 38 | 39 | - run: npm run dist 40 | if: runner.os != 'Windows' 41 | env: 42 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | CSC_LINK: ${{ secrets.APPLE_CERT_BASE64 }} 44 | CSC_KEY_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} 45 | APPLE_ID: ${{ secrets.APPLE_ID }} 46 | APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} 47 | 48 | - run: npm run dist:all-arch 49 | if: runner.os == 'Windows' 50 | env: 51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | releases 5 | installers 6 | npm-debug.log 7 | .DS_Store 8 | .tmp 9 | *.tsbuildinfo 10 | 11 | # test files 12 | *.sqlite3 13 | *.db 14 | 15 | # auto generated files during tests 16 | test/fixtures/sqlite/sqlectron.json 17 | test/fixtures/browser/tmp.sqlectron.json 18 | test/fixtures/ssh-mysql/sqlectron.json 19 | test/fixtures/simple/sqlectron.json 20 | storybook-static 21 | 22 | # Editor ignores 23 | .vscode/ 24 | .idea/ 25 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # Run linters 5 | npx --no-install lint-staged 6 | 7 | # Run test 8 | npm test 9 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx}": ["eslint --fix"], 3 | "*.{js,jsx,ts,tsx,json,css,md}": ["prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /.mocharc.yaml: -------------------------------------------------------------------------------- 1 | exit: true 2 | colors: true 3 | timeout: 10000 4 | require: test/setup.ts 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | playwright_skip_browser_download=1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | vendor/ 4 | out/ 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "all", 4 | "semi": true, 5 | "singleQuote": true, 6 | "arrowParens": "always", 7 | "jsxBracketSameLine": true 8 | } 9 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../stories/**/*.stories.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'], 3 | addons: ['@storybook/addon-links', '@storybook/addon-essentials'], 4 | core: { 5 | builder: 'webpack5', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: '^on[A-Z].*' }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Sqlectron 2 | 3 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 4 | 5 | There are some ways of contributing to Sqlectron, you can either: 6 | 7 | - Report an issue. 8 | - [Contribute to the code base](docs/development). 9 | 10 | ## Report an issue 11 | 12 | - Before opening the issue make sure there isn't an issue opened for the same problem you're facing 13 | - Include the database client you were using during the error (mysql, postgres, etc.) 14 | - Include the version of Sqlectron you are using 15 | - If possible include screenshots and/or animated GIFs 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM maxcnunes/electron-distribution:eb3node4-onbuild 2 | 3 | # Keep the tmp folder inside the project. 4 | # This way we dont lose some cache files between distribution tasks 5 | ENV TMPDIR /usr/src/app/.tmp 6 | 7 | # Build all and pack only for Windows 8 | # because is not possible packing for OSX from Linux. 9 | # Use unsafe-perm to force npm respect the tmp path (http://git.io/vB2oR) 10 | CMD ["npm", "run", "--unsafe-perm", "dist:winlinux"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 The SQLECTRON Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Slack Status](https://sqlectron.herokuapp.com/badge.svg)](https://sqlectron.herokuapp.com) 2 | [![Build](https://github.com/sqlectron/sqlectron-gui/workflows/Build/badge.svg?branch=master&event=push)](https://github.com/sqlectron/sqlectron-gui/actions?query=workflow%3ABuild+branch%3Amaster) 3 | 4 |

5 | 6 |
7 | A simple and lightweight SQL client with cross database and platform support. 8 |

9 | 10 | #### Demo (version 1.0.0) 11 | 12 | ![demo](https://sqlectron.github.io/demos/sqlectron-demo-gui-v1.0.0-small.gif) 13 | 14 | - [Databases](https://github.com/sqlectron/sqlectron-core#current-supported-databases) - List of current supported databases. 15 | - [Download](https://github.com/sqlectron/sqlectron-gui/releases) - Installers, binaries and source. 16 | - [Configuration](docs/app/configuration-file.md) - List of saved servers and custom configurations. 17 | - [App Docs](docs/app) - Helper docs about the app. 18 | - [Terminal](https://github.com/sqlectron/sqlectron-term) - A terminal-based interface of Sqlectron. 19 | - [Contribute](CONTRIBUTING.md) - Details on how you can contribute to Sqlectron. 20 | 21 | #### How to pronounce 22 | 23 | It is pronounced "sequel-eck-tron" - https://translate.google.com/?source=osdd#en/en/sequel-eck-tron 24 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: 'Chrome > 89', // Electron 12 7 | useBuiltIns: 'usage', 8 | corejs: { version: 3, proposals: true }, 9 | }, 10 | ], 11 | '@babel/preset-react', 12 | '@babel/preset-typescript', 13 | ], 14 | plugins: [ 15 | [ 16 | '@babel/plugin-proposal-class-properties', 17 | { 18 | loose: true, 19 | }, 20 | ], 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /build/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/build/app.png -------------------------------------------------------------------------------- /build/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/build/background.png -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | com.apple.security.cs.allow-jit 7 | 8 | com.apple.security.cs.allow-unsigned-executable-memory 9 | 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/build/icon.ico -------------------------------------------------------------------------------- /build/install-spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/build/install-spinner.gif -------------------------------------------------------------------------------- /build/notarize.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | const { notarize } = require('electron-notarize'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | if (fs.existsSync(path.join(__dirname, '..', '.env'))) { 7 | dotenv.config({ 8 | path: path.resolve(path.join(__dirname, '..', '.env')), 9 | }); 10 | } 11 | 12 | exports.default = async (context) => { 13 | if (process.env.SKIP_NOTARIZE === '1') { 14 | // eslint-disable-next-line no-console 15 | console.info('Skipping notarization'); 16 | return; 17 | } 18 | 19 | if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) { 20 | console.info('Could not find Apple ID or password'); 21 | return; 22 | } 23 | 24 | const { electronPlatformName, appOutDir } = context; 25 | if (electronPlatformName !== 'darwin') { 26 | return; 27 | } 28 | 29 | const appName = context.packager.appInfo.productFilename; 30 | const appPath = path.join(context.appOutDir, `${appName}.app`); 31 | 32 | // eslint-disable-next-line no-console 33 | console.info(`Notarizing ${appName} found at ${appPath}`); 34 | 35 | return await notarize({ 36 | appBundleId: 'desktop.sqlectron.app', 37 | appPath: `${appOutDir}/${appName}.app`, 38 | appleId: process.env.APPLE_ID, 39 | appleIdPassword: process.env.APPLE_ID_PASSWORD, 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | # --------------------- 5 | # Distribution services 6 | # --------------------- 7 | dist: 8 | image: maxcnunes/electron-distribution:eb3node4-onbuild 9 | # Build all and pack only for Windows 10 | # because is not possible packing for OSX from Linux. 11 | # Use unsafe-perm to force npm respect the tmp path (http://git.io/vB2oR) 12 | command: npm run --unsafe-perm dist:winlinux 13 | environment: 14 | # Keep the tmp folder inside the project. 15 | # This way we dont lose some cache files between distribution tasks 16 | TMPDIR: /usr/src/app/.tmp 17 | volumes: 18 | - .:/usr/src/app 19 | -------------------------------------------------------------------------------- /docs/app/configuration-file.md: -------------------------------------------------------------------------------- 1 | ## Configuration File 2 | 3 | SQLECTRON keeps a configuration file in the directory: 4 | 5 | - **macOS:** `~/Library/Preferences/Sqlectron` 6 | - **Linux:** (`$XDG_CONFIG_HOME` or `~/.config`) + `/Sqlectron` 7 | - **Windows:** (`$LOCALAPPDATA` or `%USERPROFILE%\AppData\Local`) + `\Sqlectron\Config` 8 | 9 | > For older versions it was stored as `.sqlectron.json` at the user's home directory: 10 | > 11 | > - **macOS:** `~/` 12 | > - **Linux:** `~/` 13 | > - **Windows:** `%USERPROFILE%` 14 | 15 | Although it is possible to change this file manually, it is usually better to just use the UI since it allows you to change any of these settings from there. 16 | 17 | ```js 18 | { 19 | // Change the zoom factor to the specified factor. 20 | // Zoom factor is zoom percent divided by 100, so 300% = 3.0. 21 | // https://github.com/electron/electron/blob/master/docs/api/web-frame.md#webframesetzoomfactorfactor 22 | "zoomFactor": 1, 23 | 24 | // Change the limit used in the default select 25 | // "100" by default in production 26 | "limitQueryDefaultSelectTop": 100, 27 | 28 | // Enable/Disable auto complete for the query box 29 | // "true" by default in production 30 | "enabledAutoComplete": false, 31 | 32 | // Enable/Disable live auto complete for the query box 33 | // "true" by default in production 34 | "enabledLiveAutoComplete": false, 35 | 36 | // Manage logging 37 | // It is disabled by default in production 38 | // More details: https://github.com/sqlectron/sqlectron-gui/blob/master/docs/app/logging.md 39 | "log": { 40 | // Show logs in the dev tools panel 41 | // "false" by default in production 42 | "console": true, 43 | 44 | // Save logs into a file 45 | // "false" by default in production 46 | "file": true, 47 | 48 | // Level logging: debug, info, warn, error 49 | // "error" by default in production 50 | "level": "debug", 51 | 52 | // Log file path 53 | // "~/.sqlectron.log" by default in production 54 | "path": "~/.sqlectron.log" 55 | }, 56 | 57 | // List of servers 58 | "servers": [ 59 | { 60 | // It's possible add a filter property that will load only 61 | // the data is useful in the sidebar. 62 | // It's only possible to filter "database" and "schema". 63 | // It accepts the filter types: "only" and "ignore". 64 | "filter": { 65 | "database": { 66 | "only": [ 67 | "company" 68 | ] 69 | }, 70 | "schema": { 71 | "ignore": [ 72 | "pg_catalog", 73 | "pg_temp_1" 74 | ] 75 | } 76 | }, 77 | "id": "651abe80-ed50-44a1-b778-1bdfe97b0bec", 78 | "name": "sqlectron-localhost", 79 | "client": "postgresql", 80 | ... 81 | } 82 | ] 83 | } 84 | 85 | ``` 86 | 87 | [Configuration doc from Sqlectron Core](https://github.com/sqlectron/sqlectron-core#configuration) 88 | -------------------------------------------------------------------------------- /docs/app/keyboard-shortcuts.md: -------------------------------------------------------------------------------- 1 | ## General 2 | 3 | | Windows/Linux | Mac | Action | 4 | | :-------------------------------------------- | :----------------------------------------------- | :----------------- | 5 | | Ctrl-Q | Command-Q | Quit application | 6 | | Ctrl-N | Command-N | New Window | 7 | | Ctrl-Z | Command-Z | Undo | 8 | | Shift-Ctrl-Z | Shift-Command-Z | Undo | 9 | | Ctrl-X | Command-X | Cut | 10 | | Ctrl-C | Command-C | Copy | 11 | | Ctrl-V | Command-V | Paste | 12 | | Ctrl-A | Command-A | Select All | 13 | | Ctrl-Shift-R | Command-Shift-R | Reload Application | 14 | | Ctrl-Alt-I | Alt-Command-I | Toggle DevTools | 15 | | | Command-M | Minimize Window | 16 | | | Command-Shift-W | Close Window | 17 | 18 | ## Find 19 | 20 | | Windows/Linux | Mac | Action | 21 | | :-------------------------------------------- | :----------------------------------------------- | :---------------------- | 22 | | Shift-Ctrl-9 | Shift-Command-9 | Search Databases | 23 | | Ctrl-9 | Command-9 | Search Database Objects | 24 | 25 | ## Query 26 | 27 | | Windows/Linux | Mac | Action | 28 | | :--------------------------------------------------------------- | :--------------------------------------------------------------------- | :----------------- | 29 | | Ctrl-T | Command-T | New Tab | 30 | | Ctrl-W | Command-W | Close Tab | 31 | | Ctrl-S | Command-S | Save Query | 32 | | Ctrl-Enter or Ctrl-R | Command-Enter or Command-R | Execute Query | 33 | | Shift-Ctrl-0 | Shift-Command-0 | Focus Query Editor | 34 | 35 | ## Query Editor 36 | 37 | | Windows/Linux | Mac | Action | 38 | | :----------------------------------- | :----------------------------------- | :------------------ | 39 | | Ctrl-L | Command-L | Select Current Line | 40 | | Ctrl-Backspace | Ctrl-Backspace | Complete word | 41 | 42 | Checkout on [Ace documentation page](https://github.com/ajaxorg/ace/wiki/Default-Keyboard-Shortcuts) the list of all available keyboard shortcuts. 43 | -------------------------------------------------------------------------------- /docs/app/logging.md: -------------------------------------------------------------------------------- 1 | ## Logging 2 | 3 | > **IMPORTANT!** Replace any sensitive data (e.g. password) before including the log data in an issue. 4 | > 5 | > **IMPORTANT!** Disable the log if you are not investigating an issue. It makes the app slower. And with log to file enabled it may saves data to file that you don't want to be saved every time you are using the app. 6 | 7 | Is possible to enable logging by adding the configuration below to your `~/.sqlectron.json`: 8 | 9 | ```js 10 | "log": { 11 | // Show logs in the dev tools panel 12 | // "false" by default in production 13 | "console": true, 14 | 15 | // Save logs into a file 16 | // "false" by default in production 17 | "file": true, 18 | 19 | // Level logging: debug, info, warn, error 20 | // "error" by default in production 21 | "level": "debug", 22 | 23 | // Log file path 24 | // "~/.sqlectron.log" by default in production 25 | "path": "~/.sqlectron.log" 26 | }, 27 | ``` 28 | 29 | The log file is pretty easy to understand by just opening it in a text editor. But if you want an even better way to see that. Is possible to use [bunyan](https://github.com/trentm/node-bunyan) CLI: 30 | 31 | > Requires to have Node and NPM installed 32 | 33 | Install bunyan CLI globally 34 | 35 | ``` 36 | npm install -g bunyan 37 | ``` 38 | 39 | See the logs with a better output through bunyan CLI 40 | 41 | ``` 42 | tail -f ~/.sqlectron.log | bunyan -o short 43 | ``` 44 | 45 | or 46 | 47 | ``` 48 | cat ~/.sqlectron.log | bunyan -o short 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/development/README.md: -------------------------------------------------------------------------------- 1 | # Sqlectron Development 2 | 3 | If you are only interested in running the Sqlectron from the source [here is](run-from-source.md) the straight forward way. 4 | 5 | After running the application if you are interested in contributing to the code base as well. Please follow these recommendations below: 6 | 7 | ## Contributing to the code base 8 | 9 | Pick [an issue](http://github.com/sqlectron/sqlectron-gui/issues) to fix, or pitch new features. To avoid wasting your time, please ask for feedback on feature suggestions either with [an issue](http://github.com/sqlectron/sqlectron-gui/issues/new) or on the Slack channel. 10 | 11 | ### Making a pull request 12 | 13 | Please try to [write great commit messages](http://chris.beams.io/posts/git-commit/). 14 | 15 | There are numerous benefits to great commit messages 16 | 17 | - They allow Sqlectron users to easily understand the consequences of updating to a newer version 18 | - They help contributors understand what is going on with the codebase, allowing features and fixes to be developed faster 19 | - They save maintainers time when compiling the changelog for a new release 20 | 21 | If you're already a few commits in by the time you read this, you can still [change your commit messages](https://help.github.com/articles/changing-a-commit-message/). 22 | 23 | Also, before making your pull request, consider if your commits make sense on their own (and potentially should be multiple pull requests) or if they can be squashed down to one commit (with a great message). There are no hard and fast rules about this, but being mindful of your readers greatly help you author good commits. 24 | 25 | ### Use EditorConfig 26 | 27 | To save everyone some time, please use [EditorConfig](http://editorconfig.org), so your editor helps make sure we all use the same encoding, indentation, line endings, etc. 28 | 29 | ### Style 30 | 31 | Sqlectron uses [ESLint](http://eslint.org) to keep consistent style. You probably want to install a plugin for your editor. 32 | 33 | The ESLint test will be run before unit tests in the CI environment, your build will fail if it doesn't pass the style check. 34 | 35 | ```bash 36 | npm run lint 37 | ``` 38 | 39 | ### Testing 40 | 41 | #### E2E Testing 42 | 43 | Start integration test dependencies: 44 | 45 | ``` 46 | ./scripts/start-test-integration-deps.sh 47 | ``` 48 | 49 | Running in production mode: 50 | 51 | ``` 52 | npm run compile 53 | DEV_TOOLS=true npm run test:e2e 54 | ``` 55 | 56 | Running in development mode: 57 | 58 | ``` 59 | npm run dev:webpack 60 | npm run test:e2e:dev 61 | ``` 62 | 63 | > Document based on [Sinon's CONTRIBUTING.md](https://github.com/sinonjs/sinon/blob/master/CONTRIBUTING.md). 64 | -------------------------------------------------------------------------------- /docs/development/build-instructions.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | 1. `npm install` 4 | 1. `npm run dist` 5 | 1. The installer will be placed at `dist` folder. 6 | -------------------------------------------------------------------------------- /docs/development/docker-databases.md: -------------------------------------------------------------------------------- 1 | # Docker databases 2 | 3 | You can test it using your own database or use a [docker-compose](https://github.com/sqlectron/sqlectron-databases) built for us to bring up several different databases. 4 | -------------------------------------------------------------------------------- /docs/development/release.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | We use [electron-builder](https://github.com/electron-userland/electron-builder) to package and distribute the application. Which makes really easy doing that. 4 | 5 | 1. Merge the changes to the main branch. 6 | 1. On GitHub, open the Build action for the last commit and test the app, it will have uploaded the artifacts for each OS. 7 | 1. Once you confirm the app is working as expected, then it is time to start the release process. 8 | 1. Run `npm version ` based on the level of the changes being deployed. It will set a new version in the package.json and also create a new git tag for that. 9 | 1. Run `git push --follow-tags` to push those changes to GitHub. 10 | 1. Create a new release for that version at https://github.com/sqlectron/sqlectron-gui/releases. Don't forget to set the release title and the description with all the changes. **DO NOT CREATE A DRAFT RELEASE, PUBLISH IT DIRECTLY**, it won't trigger the Release action to upload the artificats because of [this issue](https://github.community/t/workflow-set-for-on-release-not-triggering-not-showing-up/16286/10). 11 | 1. Once the release is created, the GitHub Release action will upload the artifacts for that build. 12 | 1. Once the release artifacts have been uploaded, then update [Homebrew Cask](https://github.com/caskroom/homebrew-cask/blob/master/CONTRIBUTING.md#updating-a-cask). 13 | -------------------------------------------------------------------------------- /docs/development/run-from-source.md: -------------------------------------------------------------------------------- 1 | # Run from Source 2 | 3 | The Sqlectron developer environment requires Node **(10 or higher)** and NPM. 4 | 5 | **Cloning this project:** 6 | 7 | ```shell 8 | git clone git@github.com:sqlectron/sqlectron-gui.git 9 | cd sqlectron-gui 10 | ``` 11 | 12 | **Installing the dependencies:** 13 | 14 | ```shell 15 | npm install 16 | ``` 17 | 18 | **Running the application:** 19 | 20 | ```shell 21 | npm run dev 22 | ``` 23 | 24 | Note: Due to timing issues between starting the webpack-dev-server and electron, it 25 | is probable that on initial start, the electron window will be empty. After waiting 26 | for webpack to finish building the bundle for the first time, you will need to 27 | reload the electron window. This can be accomplished using `Shift+Cmd/Ctrl+R`. 28 | -------------------------------------------------------------------------------- /docs/development/test-core-changes.md: -------------------------------------------------------------------------------- 1 | # Testing changes of sqlectron-db-core 2 | 3 | This is an easy way to test sqlectron-db-core changes on the GUI project. 4 | 5 | 1. Make the GUI project use the core from `../sqlectron-db-core` directory: 6 | 7 | ```bash 8 | # from sqlectron-gui directory 9 | ./scripts/link-sqlectron-db-core.sh 10 | ``` 11 | 12 | 1. Start the auto compile of sqlectron-db-core: 13 | 14 | ```bash 15 | # from sqlectron-db-core directory 16 | npm run watch 17 | ``` 18 | 19 | 1. Then run the GUI project normally: 20 | 21 | ```shell 22 | # from sqlectron-gui directory 23 | npm run dev 24 | ``` 25 | -------------------------------------------------------------------------------- /empty-shim.js: -------------------------------------------------------------------------------- 1 | module.exports = null; 2 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | 3 | module.exports = { 4 | plugins: { 5 | autoprefixer, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-console": 0, 7 | "prefer-destructuring": 0 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /scripts/dev-auto-restart.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const fs = require('fs'); 3 | const { join } = require('path'); 4 | const electron = require('electron'); 5 | 6 | const main = join(__dirname, '../out/browser/main.js'); 7 | const watch = [join(__dirname, '../out/browser')]; 8 | 9 | const pathCore = join(__dirname, '../../sqlectron-db-core/lib'); 10 | try { 11 | fs.accessSync(pathCore, fs.F_OK); 12 | watch.push(pathCore); 13 | } catch (err) { 14 | console.log('Not watching changes on sqlectron-db-core'); 15 | } 16 | 17 | require('spawn-auto-restart')({ 18 | debug: true, 19 | proc: { 20 | command: electron, 21 | args: [main, '--dev'], 22 | }, 23 | watch, 24 | }); 25 | -------------------------------------------------------------------------------- /scripts/link-sqlectron-db-core.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | rm -rf node_modules/sqlectron-db-core 5 | npm link ../sqlectron-db-core 6 | -------------------------------------------------------------------------------- /scripts/start-test-integration-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | cd .. 5 | 6 | if [ ! -d "sqlectron-db-core" ]; then 7 | # Only clone flat in the CI. In development the developer probably wants to 8 | # use the sqlectron-db-core for other changes as well and may need the full history. 9 | if [ "$CI" == "true" ]; then 10 | git clone --depth 1 https://github.com/sqlectron/sqlectron-db-core.git 11 | else 12 | git clone https://github.com/sqlectron/sqlectron-db-core.git 13 | fi 14 | fi 15 | 16 | cd sqlectron-db-core 17 | docker-compose up -d openssh-server mysql 18 | 19 | # Given a few seconds for the all deps be ready 20 | sleep 10 21 | -------------------------------------------------------------------------------- /src/browser/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module", 4 | "ecmaVersion": 2020 5 | }, 6 | "extends": ["eslint:recommended", "prettier"], 7 | "plugins": ["prettier"], 8 | "rules": { 9 | "prettier/prettier": ["error"] 10 | }, 11 | "env": { 12 | "node": true, 13 | "mocha": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/browser/app.ts: -------------------------------------------------------------------------------- 1 | import { app, dialog } from 'electron'; 2 | import installExtension, { 3 | REACT_DEVELOPER_TOOLS, 4 | REDUX_DEVTOOLS, 5 | } from 'electron-devtools-installer'; 6 | 7 | import createLogger from './logger'; 8 | import { buildNewWindow } from './window'; 9 | import { registerIPCMainHandlers } from './ipcMain'; 10 | 11 | async function loadExtension(extension) { 12 | try { 13 | const name = await installExtension(extension); 14 | // eslint-disable-next-line no-console 15 | console.log(`Loaded ${name}`); 16 | } catch (err) { 17 | // eslint-disable-next-line no-console 18 | console.error(`Error loading extension: ${err}`); 19 | } 20 | } 21 | 22 | const logger = createLogger('app'); 23 | 24 | // TODO: Create a server to receive the crash reports 25 | // Report crashes to our server. 26 | // require('crash-reporter').start({ 27 | // productName: 'Sqlectron', 28 | // companyName: 'Sqlectron', 29 | // submitURL: 'https://your-domain.com/url-to-submit', 30 | // autoSubmit: true 31 | // }); 32 | 33 | app.allowRendererProcessReuse = false; 34 | 35 | // Quit when all windows are closed. 36 | app.on('window-all-closed', () => { 37 | if (process.platform !== 'darwin') { 38 | app.quit(); 39 | } 40 | }); 41 | 42 | // This method will be called when Electron has done everything 43 | // initialization and ready for creating browser windows. 44 | app.whenReady().then(async () => { 45 | registerIPCMainHandlers(); 46 | 47 | if (process.env.NODE_ENV === 'development' || process.env.DEV_TOOLS === 'true') { 48 | await Promise.all([loadExtension(REACT_DEVELOPER_TOOLS), loadExtension(REDUX_DEVTOOLS)]); 49 | } 50 | 51 | buildNewWindow(app); 52 | }); 53 | 54 | app.on('web-contents-created', (event, contents) => { 55 | contents.on('will-navigate', (event) => { 56 | // Disables in-app navigation 57 | event.preventDefault(); 58 | }); 59 | 60 | contents.setWindowOpenHandler(() => { 61 | // Disables opening new windows with window.open() 62 | return { action: 'deny' }; 63 | }); 64 | }); 65 | 66 | // Show only the error description to the user 67 | process.on('uncaughtException', (error) => { 68 | logger.error('uncaughtException', error); 69 | return dialog.showErrorBox('An error occurred', error.name + ': ' + error.message); 70 | }); 71 | -------------------------------------------------------------------------------- /src/browser/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Load app configurations. 3 | * 4 | * Since it may be loaded directly from the renderer process, 5 | * without passing through a transpiler, this file must use ES5. 6 | */ 7 | 8 | import fs from 'fs'; 9 | import path from 'path'; 10 | import defaultsDeep from 'lodash/defaultsDeep'; 11 | import * as sqlectron from './core'; 12 | import { Config } from '../common/types/config'; 13 | 14 | let config: Config; 15 | 16 | const cryptoSecret = 'j[F6Y6NoWT}+YG|4c|-<89:ByJ83-9Aj?O8>$Zk/[WFk_~gFbg7{ 7 | servers: [], 8 | }; 9 | 10 | function sanitizeServer(server, cryptoSecret) { 11 | const srv = { ...server }; 12 | 13 | // ensure has an unique id 14 | if (!srv.id) { 15 | srv.id = uuidv4(); 16 | } 17 | 18 | // ensure has the new fileld SSL 19 | srv.ssl = srv.ssl || false; 20 | 21 | // ensure all secret fields are encrypted 22 | if (typeof srv.encrypted === 'undefined') { 23 | srv.encrypted = true; 24 | 25 | if (srv.password) { 26 | srv.password = crypto.encrypt(srv.password, cryptoSecret); 27 | } 28 | 29 | if (srv.ssh && srv.ssh.password) { 30 | srv.ssh.password = crypto.encrypt(srv.ssh.password, cryptoSecret); 31 | } 32 | } else if (srv.encrypted) { 33 | if (srv.password && typeof srv.password === 'string') { 34 | srv.password = crypto.encrypt(crypto.unsafeDecrypt(srv.password, cryptoSecret), cryptoSecret); 35 | } 36 | if (srv.ssh && srv.ssh.password && typeof srv.ssh.password === 'string') { 37 | srv.ssh.password = crypto.encrypt( 38 | crypto.unsafeDecrypt(srv.ssh.password, cryptoSecret), 39 | cryptoSecret, 40 | ); 41 | } 42 | } 43 | 44 | return srv; 45 | } 46 | 47 | function sanitizeServers(data, cryptoSecret) { 48 | return data.servers.map((server) => sanitizeServer(server, cryptoSecret)); 49 | } 50 | 51 | /** 52 | * Prepare the configuration file sanitizing and validating all fields availbale 53 | */ 54 | export async function prepare(cryptoSecret: string): Promise { 55 | const filename = utils.getConfigPath(); 56 | const fileExistsResult = await utils.fileExists(filename); 57 | if (!fileExistsResult) { 58 | await utils.createParentDirectory(filename); 59 | await utils.writeJSONFile(filename, EMPTY_CONFIG); 60 | } 61 | 62 | const result = await utils.readJSONFile(filename); 63 | 64 | result.servers = sanitizeServers(result, cryptoSecret); 65 | 66 | await utils.writeJSONFile(filename, result); 67 | 68 | // TODO: Validate whole configuration file 69 | // if (!configValidate(result)) { 70 | // throw new Error('Invalid ~/.sqlectron.json file format'); 71 | // } 72 | } 73 | 74 | export function prepareSync(cryptoSecret: string): void { 75 | const filename = utils.getConfigPath(); 76 | const fileExistsResult = utils.fileExistsSync(filename); 77 | if (!fileExistsResult) { 78 | utils.createParentDirectorySync(filename); 79 | utils.writeJSONFileSync(filename, EMPTY_CONFIG); 80 | } 81 | 82 | const result = utils.readJSONFileSync(filename); 83 | 84 | result.servers = sanitizeServers(result, cryptoSecret); 85 | 86 | utils.writeJSONFileSync(filename, result); 87 | 88 | // TODO: Validate whole configuration file 89 | // if (!configValidate(result)) { 90 | // throw new Error('Invalid ~/.sqlectron.json file format'); 91 | // } 92 | } 93 | 94 | export function path(): string { 95 | const filename = utils.getConfigPath(); 96 | return utils.resolveHomePathToAbsolute(filename); 97 | } 98 | 99 | export function get(): Promise { 100 | const filename = utils.getConfigPath(); 101 | return utils.readJSONFile(filename); 102 | } 103 | 104 | export function getSync(): ConfigFile { 105 | const filename = utils.getConfigPath(); 106 | return utils.readJSONFileSync(filename); 107 | } 108 | 109 | export function save(data: Config): Promise { 110 | const filename = utils.getConfigPath(); 111 | return utils.writeJSONFile(filename, data); 112 | } 113 | 114 | export async function saveSettings(data: BaseConfig): Promise { 115 | const fullData = await get(); 116 | const filename = utils.getConfigPath(); 117 | const newData = { ...fullData, ...data }; 118 | return utils.writeJSONFile(filename, newData); 119 | } 120 | -------------------------------------------------------------------------------- /src/browser/core/crypto.ts: -------------------------------------------------------------------------------- 1 | // Reference: http://lollyrock.com/articles/nodejs-encryption 2 | import crypto from 'crypto'; 3 | import { EncryptedPassword } from '../../common/types/server'; 4 | 5 | const algorithm = 'aes-256-cbc'; 6 | 7 | export function encrypt(plainText: string, secret: string): EncryptedPassword { 8 | if (!plainText) { 9 | throw new Error('Missing plain text'); 10 | } else if (!secret) { 11 | throw new Error('Missing encrypt secret'); 12 | } 13 | let key = Buffer.alloc(32); 14 | key = Buffer.concat([Buffer.from(secret)], key.length); 15 | const ivBytes = crypto.randomBytes(16); 16 | const cipher = crypto.createCipheriv(algorithm, key, ivBytes); 17 | return { 18 | ivText: ivBytes.toString('base64'), 19 | encryptedText: cipher.update(plainText, 'utf8', 'base64') + cipher.final('base64'), 20 | }; 21 | } 22 | 23 | export function decrypt(encrypted: EncryptedPassword, secret: string): string { 24 | if (!encrypted || !encrypted.ivText || !encrypted.encryptedText) { 25 | throw new Error('Invalid encrypted valued'); 26 | } else if (!secret) { 27 | throw new Error('Missing decrypt secret'); 28 | } 29 | 30 | const iv = Buffer.from(encrypted.ivText, 'base64'); 31 | let key = Buffer.alloc(32); 32 | key = Buffer.concat([Buffer.from(secret)], key.length); 33 | 34 | if (iv.length !== 16) { 35 | throw new Error('The encrypted value is not a valid format'); 36 | } 37 | 38 | if (key.length !== 32) { 39 | throw new Error('The secret is not valid format'); 40 | } 41 | 42 | const decipher = crypto.createDecipheriv(algorithm, key, iv); 43 | return decipher.update(encrypted.encryptedText, 'base64', 'utf8') + decipher.final('utf8'); 44 | } 45 | 46 | /** 47 | * Decrypts a value using the insecure createDecipher method. 48 | * 49 | * This method should not be used and only exists to give an upgrade path 50 | * of existing sqlectron-db-core users to the much better iv functions above. 51 | * 52 | * @deprecated 8.1.0 53 | * @param {string} text 54 | * @param {string} secret 55 | */ 56 | export function unsafeDecrypt(text: string, secret: string): string { 57 | if (!secret) { 58 | throw new Error('Missing crypto secret'); 59 | } 60 | 61 | const decipher = crypto.createDecipher('aes-256-ctr', secret); 62 | return decipher.update(text, 'hex', 'utf8') + decipher.final('utf8'); 63 | } 64 | -------------------------------------------------------------------------------- /src/browser/core/index.ts: -------------------------------------------------------------------------------- 1 | import { setLogger } from 'sqlectron-db-core'; 2 | import { getConn } from './db'; 3 | import * as config from './config'; 4 | import * as servers from './servers'; 5 | import { setSelectLimit } from './limit'; 6 | 7 | export { config, servers, getConn, setLogger, setSelectLimit }; 8 | -------------------------------------------------------------------------------- /src/browser/core/limit.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setSelectLimit as internalSet, 3 | clearSelectLimit as internalClear, 4 | } from 'sqlectron-db-core'; 5 | import * as config from './config'; 6 | 7 | export async function setSelectLimit(): Promise { 8 | const { limitQueryDefaultSelectTop } = await config.get(); 9 | if (limitQueryDefaultSelectTop !== null && limitQueryDefaultSelectTop !== undefined) { 10 | internalSet(limitQueryDefaultSelectTop); 11 | } 12 | } 13 | 14 | export function clearSelectLimit(): void { 15 | internalClear(); 16 | } 17 | -------------------------------------------------------------------------------- /src/browser/core/utils/index.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { homedir } from 'os'; 3 | import path from 'path'; 4 | import envPaths from 'env-paths'; 5 | 6 | let configPath = ''; 7 | 8 | export function getConfigPath(): string { 9 | if (configPath) { 10 | return configPath; 11 | } 12 | 13 | const configName = 'sqlectron.json'; 14 | const oldConfigPath = path.join(homedir(), `.${configName}`); 15 | 16 | if (process.env.SQLECTRON_HOME) { 17 | configPath = path.join(process.env.SQLECTRON_HOME, configName); 18 | } else if (fileExistsSync(oldConfigPath)) { 19 | configPath = oldConfigPath; 20 | } else { 21 | const newConfigDir = envPaths('Sqlectron', { suffix: '' }).config; 22 | configPath = path.join(newConfigDir, configName); 23 | } 24 | 25 | return configPath; 26 | } 27 | 28 | export function fileExists(filename: string): Promise { 29 | return new Promise((resolve) => { 30 | fs.stat(filename, (err, stats) => { 31 | if (err) return resolve(false); 32 | resolve(stats.isFile()); 33 | }); 34 | }); 35 | } 36 | 37 | export function fileExistsSync(filename: string): boolean { 38 | try { 39 | return fs.statSync(filename).isFile(); 40 | } catch (e) { 41 | return false; 42 | } 43 | } 44 | 45 | export function writeFile(filename: string, data: string): Promise { 46 | return new Promise((resolve, reject) => { 47 | fs.writeFile(filename, data, (err) => { 48 | if (err) return reject(err); 49 | resolve(); 50 | }); 51 | }); 52 | } 53 | 54 | export function writeJSONFile(filename: string, data: T): Promise { 55 | return writeFile(filename, JSON.stringify(data, null, 2)); 56 | } 57 | 58 | export function writeJSONFileSync(filename: string, data: T): void { 59 | return fs.writeFileSync(filename, JSON.stringify(data, null, 2)); 60 | } 61 | 62 | export function readFile(filename: string): Promise { 63 | return new Promise((resolve, reject) => { 64 | fs.readFile(filename, { encoding: 'utf-8' }, (err, data) => { 65 | if (err) return reject(err); 66 | resolve(data); 67 | }); 68 | }); 69 | } 70 | 71 | export async function readJSONFile(filename: string): Promise { 72 | const filePath = resolveHomePathToAbsolute(filename); 73 | const data = await readFile(path.resolve(filePath)); 74 | return JSON.parse(data); 75 | } 76 | 77 | export function readJSONFileSync(filename: string): T { 78 | const filePath = resolveHomePathToAbsolute(filename); 79 | const data = fs.readFileSync(path.resolve(filePath), { encoding: 'utf-8' }); 80 | return JSON.parse(data); 81 | } 82 | 83 | export function createParentDirectory(filename: string): Promise { 84 | return new Promise((resolve, reject) => { 85 | fs.mkdir(filename, { recursive: true }, (err) => { 86 | if (err) { 87 | return reject(err); 88 | } 89 | resolve(); 90 | }); 91 | }); 92 | } 93 | 94 | export function createParentDirectorySync(filename: string): void { 95 | fs.mkdirSync(path.dirname(filename), { recursive: true }); 96 | } 97 | 98 | export function resolveHomePathToAbsolute(filename: string): string { 99 | if (!/^~\//.test(filename)) { 100 | return filename; 101 | } 102 | 103 | return path.join(homedir(), filename.substring(2)); 104 | } 105 | -------------------------------------------------------------------------------- /src/browser/core/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export default function wait(time: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, time)); 3 | } 4 | -------------------------------------------------------------------------------- /src/browser/logger.ts: -------------------------------------------------------------------------------- 1 | import * as sqlectron from './core'; 2 | import { getConfig } from './config'; 3 | 4 | // Hack solution to ignore console.error from dtrace imported by bunyan 5 | /* eslint no-console:0 */ 6 | const realConsoleError = console.error; 7 | // eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function 8 | console.error = (message?: any, ...optionalParams: any[]) => {}; 9 | import { createLogger } from 'bunyan'; 10 | 11 | console.error = realConsoleError; 12 | 13 | const dataConfig = getConfig(); 14 | 15 | export interface LogStream { 16 | path?: string; 17 | stream?: NodeJS.WriteStream; 18 | } 19 | 20 | export interface LoggerConfig { 21 | app: string; 22 | name: string; 23 | level: string; 24 | streams: Array; 25 | } 26 | 27 | const loggerConfig: LoggerConfig = { 28 | app: 'sqlectron-gui', 29 | name: 'sqlectron-gui', 30 | level: dataConfig.log.level, 31 | streams: [], 32 | }; 33 | 34 | if (dataConfig.log.console) { 35 | loggerConfig.streams.push({ stream: process.stdout }); 36 | } 37 | 38 | if (dataConfig.log.file) { 39 | loggerConfig.streams.push({ path: dataConfig.log.path }); 40 | } 41 | 42 | const logger = createLogger(loggerConfig); 43 | 44 | // Set custom logger for sqlectron-core 45 | sqlectron.setLogger((namespace) => logger.child({ namespace: `sqlectron-core:${namespace}` })); 46 | 47 | export default (namespace: string): Console => logger.child({ namespace }); 48 | -------------------------------------------------------------------------------- /src/browser/main.ts: -------------------------------------------------------------------------------- 1 | import * as sqlectron from './core'; 2 | import { getConfig } from './config'; 3 | 4 | const configData = getConfig(); 5 | 6 | if (configData.printVersion) { 7 | console.log(configData.name, configData.version); // eslint-disable-line no-console 8 | process.exit(0); 9 | } 10 | 11 | if ( 12 | configData.limitQueryDefaultSelectTop !== undefined && 13 | configData.limitQueryDefaultSelectTop !== null 14 | ) { 15 | sqlectron.setSelectLimit(); 16 | } 17 | 18 | // starts the electron app 19 | import './app'; 20 | -------------------------------------------------------------------------------- /src/browser/menu/index.ts: -------------------------------------------------------------------------------- 1 | import { Menu, App } from 'electron'; 2 | import { Config } from '../../common/types/config'; 3 | import { BuildWindow } from '../../common/types/menu'; 4 | import * as darwin from './darwin'; 5 | import * as linux from './linux'; 6 | import * as win32 from './win32'; 7 | 8 | const menus = { 9 | darwin, 10 | linux, 11 | win32, 12 | }; 13 | 14 | export function attachMenuToWindow(app: App, buildNewWindow: BuildWindow, appConfig: Config): void { 15 | const template = menus[process.platform].buildTemplate(app, buildNewWindow, appConfig); 16 | const menu = Menu.buildFromTemplate(template); 17 | Menu.setApplicationMenu(menu); 18 | 19 | if (process.platform === 'darwin') { 20 | const dockTemplate = menus.darwin.buildTemplateDockMenu(app, buildNewWindow); 21 | const dockMenu = Menu.buildFromTemplate(dockTemplate); 22 | app.dock.setMenu(dockMenu); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/browser/update-checker.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { BrowserWindow } from 'electron'; 3 | import { Config } from '../common/types/config'; 4 | import createLogger from './logger'; 5 | import * as event from '../common/event'; 6 | 7 | const logger = createLogger('gh-update-checker'); 8 | 9 | export async function check(mainWindow: BrowserWindow, appConfig: Config): Promise { 10 | const currentVersion = `v${appConfig.version}`; 11 | logger.debug('current version %s', currentVersion); 12 | 13 | const repo = appConfig.repository?.url.replace('https://github.com/', ''); 14 | const latestReleaseURL = `https://api.github.com/repos/${repo}/releases/latest`; 15 | const response = await axios.get(latestReleaseURL); 16 | 17 | logger.debug('latest version %s', response.data.tag_name); 18 | 19 | if (currentVersion === response.data.tag_name) { 20 | logger.debug('already using the latest version'); 21 | return; 22 | } 23 | 24 | mainWindow.webContents.send(event.UPDATE_AVAILABLE, currentVersion, response.data.tag_name); 25 | } 26 | -------------------------------------------------------------------------------- /src/browser/window.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { BrowserWindow, ipcMain, Menu, App } from 'electron'; 3 | import { attachMenuToWindow } from './menu'; 4 | import { check as checkUpdate } from './update-checker'; 5 | import { getConfig } from './config'; 6 | import createLogger from './logger'; 7 | 8 | const logger = createLogger('window'); 9 | 10 | const devMode = (process.argv || []).indexOf('--dev') !== -1; 11 | 12 | // Keep a global reference of the window object, if you don't, the window will 13 | // be closed automatically when the javascript object is GCed. 14 | const WINDOWS = {}; 15 | 16 | // Indicate the number of windows has already been opened. 17 | // Also used as identifier to for each window. 18 | let windowsNumber = 0; 19 | 20 | export function buildNewWindow(app: App): void { 21 | const appConfig = getConfig(); 22 | 23 | windowsNumber += 1; 24 | const mainWindow = new BrowserWindow({ 25 | title: appConfig.name, 26 | icon: resolve(__dirname, '..', '..', 'build', 'app.png'), 27 | width: 1024, 28 | height: 700, 29 | minWidth: 512, 30 | minHeight: 350, 31 | webPreferences: { 32 | preload: resolve(__dirname, 'preload.js'), 33 | }, 34 | }); 35 | 36 | attachMenuToWindow(app, buildNewWindow, appConfig); 37 | 38 | // and load the index.html of the app. 39 | let entryBasePath = 'file://' + resolve(__dirname, '..'); 40 | if (devMode) { 41 | entryBasePath = 'http://localhost:9000'; 42 | } 43 | 44 | const appUrl = entryBasePath + '/static/index.html'; 45 | 46 | mainWindow.loadURL(appUrl); 47 | 48 | // block navigation that would lead outside the application 49 | mainWindow.webContents.on('will-navigate', (e, url) => { 50 | if (url === appUrl) { 51 | return; 52 | } 53 | e.preventDefault(); 54 | }); 55 | 56 | // Emitted when the window is closed. 57 | mainWindow.on('closed', () => delete WINDOWS[windowsNumber]); 58 | 59 | if (devMode || process.env.DEV_TOOLS === 'true') { 60 | mainWindow.webContents.openDevTools(); 61 | mainWindow.webContents.on('context-menu', (_, props) => { 62 | const { x, y } = props; 63 | Menu.buildFromTemplate([ 64 | { 65 | label: 'Inspect element', 66 | click() { 67 | mainWindow.webContents.inspectElement(x, y); 68 | }, 69 | }, 70 | ]).popup({ window: mainWindow }); 71 | }); 72 | } 73 | 74 | ipcMain.on('sqlectron:check-upgrade', () => { 75 | checkUpdate(mainWindow, appConfig).catch((err) => 76 | logger.error('Unable to check for updates', err), 77 | ); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/common/types/config.ts: -------------------------------------------------------------------------------- 1 | import { Server } from './server'; 2 | 3 | interface LogOptions { 4 | console: boolean; 5 | file: boolean; 6 | level: string; 7 | path: string; 8 | } 9 | 10 | /** 11 | * BaseConfig object, which represents what we can save from the settings menu, 12 | * as well as what might (mostly) be in the config file. 13 | */ 14 | export interface BaseConfig { 15 | log?: LogOptions; 16 | zoomFactor?: number; 17 | limitQueryDefaultSelectTop?: number; 18 | enabledAutoComplete?: boolean; 19 | enabledLiveAutoComplete?: boolean; 20 | // queries: Object; 21 | enabledDarkTheme?: boolean; 22 | disabledOpenAnimation?: boolean; 23 | csvDelimiter?: string; 24 | connectionsAsList?: boolean; 25 | customFont?: string; 26 | } 27 | 28 | /** 29 | * This interface documents the sqlectron.json file. The only thing guaranteed to 30 | * exist is the `servers` key/value pair (instantiated to empty array). The rest 31 | * will only exist if the user goes into the Settings modal and hits save. 32 | */ 33 | export interface ConfigFile extends BaseConfig { 34 | servers: Array; 35 | } 36 | 37 | /** 38 | * This interface documents the instantiated Config object that is fed through 39 | * sqlectron application. This takes the config loaded by the file, adds in application 40 | * defaults and fields from package.json, and then utilizes that throughout. 41 | */ 42 | export interface Config extends ConfigFile { 43 | log: LogOptions; 44 | zoomFactor: number; 45 | limitQueryDefaultSelectTop: number; 46 | enabledAutoComplete: boolean; 47 | enabledLiveAutoComplete: boolean; 48 | // queries: Object; 49 | enabledDarkTheme: boolean; 50 | disabledOpenAnimation: boolean; 51 | csvDelimiter: string; 52 | connectionsAsList: boolean; 53 | customFont: string; 54 | // Fields attached from package.json 55 | name: string; 56 | version: string; 57 | homepage?: string; 58 | bugs?: string; 59 | repository?: { 60 | url: string; 61 | }; 62 | // Fields attached from cli args 63 | devMode?: boolean; 64 | printVersion?: boolean; 65 | // Fields attached in runtime by the config setup 66 | crypto?: { 67 | secret: string; 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /src/common/types/database.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter as SqlectronAdapter } from 'sqlectron-db-core'; 2 | import type { ListTableResult, ListViewResult } from 'sqlectron-db-core/adapters/abstract_adapter'; 3 | 4 | export type { Database, SchemaFilter, DatabaseFilter } from 'sqlectron-db-core'; 5 | 6 | export type Adapter = Omit; 7 | 8 | export type DbTable = ListTableResult; 9 | export type DbView = ListViewResult; 10 | -------------------------------------------------------------------------------- /src/common/types/menu.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'electron'; 2 | 3 | export type BuildWindow = (app: App) => void; // eslint-disable-line no-unused-vars 4 | -------------------------------------------------------------------------------- /src/common/types/server.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseFilter, SchemaFilter } from 'sqlectron-db-core'; 2 | 3 | export interface EncryptedPassword { 4 | ivText: string; 5 | encryptedText: string; 6 | } 7 | 8 | export interface ServerResult { 9 | data?: Server; 10 | validationErrors?: unknown; 11 | } 12 | 13 | export interface Server { 14 | id: string; 15 | name: string; 16 | client: string; 17 | adapter?: string; 18 | host?: string; 19 | socketPath?: string; 20 | port?: number; 21 | localHost?: string; 22 | localPort?: number; 23 | user?: string; 24 | password: EncryptedPassword | string; 25 | applicationName?: string; 26 | domain?: string; 27 | ssh?: { 28 | user: string; 29 | password: EncryptedPassword | string | null; 30 | passphrase?: string; 31 | privateKey?: string | null; 32 | host: string; 33 | port: number; 34 | privateKeyWithPassphrase?: boolean; 35 | useAgent?: boolean; 36 | }; 37 | ssl?: 38 | | { 39 | key?: string; 40 | ca?: string; 41 | cert?: string; 42 | } 43 | | boolean; 44 | encrypted?: boolean; 45 | database: string; 46 | schema?: string; 47 | filter?: DatabaseFilter & SchemaFilter; 48 | } 49 | -------------------------------------------------------------------------------- /src/common/utils/convert.ts: -------------------------------------------------------------------------------- 1 | export function rowsValuesToString(rows: any[]): any[] { 2 | return rows.map((row) => rowValuesToString(row)); 3 | } 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-types 6 | export function rowValuesToString(row: object | any[]): unknown { 7 | if (Array.isArray(row)) { 8 | return rowsValuesToString(row); 9 | } 10 | 11 | const parsedRow = {}; 12 | 13 | Object.keys(row).forEach((col) => { 14 | parsedRow[col] = valueToString(row[col]); 15 | }); 16 | 17 | return parsedRow; 18 | } 19 | 20 | export function valueToString(value: unknown): string { 21 | if (value === null) { 22 | return 'NULL'; 23 | } 24 | 25 | if (typeof value === 'boolean') { 26 | return value.toString(); 27 | } 28 | 29 | if (!value) { 30 | return String(value); 31 | } 32 | 33 | if (value instanceof Date && value.toISOString) { 34 | return value.toISOString(); 35 | } 36 | 37 | if (typeof value === 'object') { 38 | if (isArrayBuffer(value)) { 39 | return arrayBufferToString(value); 40 | } 41 | 42 | return JSON.stringify(value); 43 | } 44 | 45 | return String(value); 46 | } 47 | 48 | function arrayBufferToString(buf: ArrayBuffer): string { 49 | // @ts-ignore 50 | if (buf.length === 1) { 51 | // Probably is a bit column 52 | return String(buf[0]); 53 | } 54 | // @ts-ignore 55 | return buf.toString('utf-8'); 56 | } 57 | 58 | // reference: 59 | // http://stackoverflow.com/a/21799845/1050818 60 | function isArrayBuffer(value: any): value is ArrayBuffer { 61 | return value && value.buffer instanceof ArrayBuffer && value.byteLength !== undefined; 62 | } 63 | -------------------------------------------------------------------------------- /src/common/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const titlize = (str: string): string => { 2 | return str[0].toUpperCase() + str.substring(1).toLowerCase(); 3 | }; 4 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.gif'; 2 | declare module '*.png'; 3 | -------------------------------------------------------------------------------- /src/renderer/actions/columns.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | import { ApplicationState, ThunkResult } from '../reducers'; 3 | 4 | export const FETCH_COLUMNS_REQUEST = 'FETCH_COLUMNS_REQUEST'; 5 | export const FETCH_COLUMNS_SUCCESS = 'FETCH_COLUMNS_SUCCESS'; 6 | export const FETCH_COLUMNS_FAILURE = 'FETCH_COLUMNS_FAILURE'; 7 | 8 | export function fetchTableColumnsIfNeeded( 9 | database: string, 10 | table: string, 11 | schema?: string, 12 | ): ThunkResult { 13 | return (dispatch, getState) => { 14 | if (shouldFetchTableColumns(getState(), database, table)) { 15 | dispatch(fetchTableColumns(database, table, schema)); 16 | } 17 | }; 18 | } 19 | 20 | function shouldFetchTableColumns( 21 | state: ApplicationState, 22 | database: string, 23 | table: string, 24 | ): boolean { 25 | const columns = state.columns; 26 | if (!columns) return true; 27 | if (columns.isFetching[database] && columns.isFetching[database][table]) return false; 28 | if (!columns.columnsByTable[database]) return true; 29 | if (!columns.columnsByTable[database][table]) return true; 30 | return columns.didInvalidate; 31 | } 32 | 33 | function fetchTableColumns(database: string, table: string, schema?: string): ThunkResult { 34 | return async (dispatch) => { 35 | dispatch({ type: FETCH_COLUMNS_REQUEST, database, table }); 36 | try { 37 | const columns = await sqlectron.db.listTableColumns(database, table, schema); 38 | dispatch({ 39 | type: FETCH_COLUMNS_SUCCESS, 40 | database, 41 | table, 42 | columns, 43 | }); 44 | } catch (error) { 45 | dispatch({ type: FETCH_COLUMNS_FAILURE, error }); 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/actions/config.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux'; 2 | import { ThunkResult } from '../reducers'; 3 | import { sqlectron } from '../api'; 4 | import { BaseConfig } from '../../common/types/config'; 5 | 6 | export const LOAD_CONFIG_REQUEST = 'LOAD_CONFIG_REQUEST'; 7 | export const LOAD_CONFIG_SUCCESS = 'LOAD_CONFIG_SUCCESS'; 8 | export const LOAD_CONFIG_FAILURE = 'LOAD_CONFIG_FAILURE'; 9 | export const SAVE_CONFIG_REQUEST = 'SAVE_CONFIG_REQUEST'; 10 | export const SAVE_CONFIG_SUCCESS = 'SAVE_CONFIG_SUCCESS'; 11 | export const SAVE_CONFIG_FAILURE = 'SAVE_CONFIG_FAILURE'; 12 | export const START_EDITING_CONFIG = 'START_EDITING_CONFIG'; 13 | export const FINISH_EDITING_CONFIG = 'FINISH_EDITING_CONFIG'; 14 | 15 | export function loadConfig(): ThunkResult { 16 | return async (dispatch) => { 17 | dispatch({ type: LOAD_CONFIG_REQUEST }); 18 | try { 19 | const forceCleanCache = true; 20 | const configPath = await sqlectron.config.path(); 21 | const config = await sqlectron.config.getFull(forceCleanCache); 22 | 23 | dispatch({ type: LOAD_CONFIG_SUCCESS, config, path: configPath }); 24 | } catch (error) { 25 | dispatch({ type: LOAD_CONFIG_FAILURE, error }); 26 | } 27 | }; 28 | } 29 | 30 | export function saveConfig(configData: BaseConfig): ThunkResult { 31 | return async (dispatch) => { 32 | dispatch({ type: SAVE_CONFIG_REQUEST }); 33 | try { 34 | await sqlectron.config.saveSettings(configData); 35 | if ( 36 | configData.limitQueryDefaultSelectTop !== null && 37 | configData.limitQueryDefaultSelectTop !== undefined 38 | ) { 39 | sqlectron.db.setSelectLimit(configData.limitQueryDefaultSelectTop); 40 | } 41 | dispatch({ type: SAVE_CONFIG_SUCCESS, config: configData }); 42 | } catch (error) { 43 | dispatch({ type: SAVE_CONFIG_FAILURE, error }); 44 | } 45 | }; 46 | } 47 | 48 | export function startEditing(): AnyAction { 49 | return { type: START_EDITING_CONFIG }; 50 | } 51 | 52 | export function finishEditing(): AnyAction { 53 | return { type: FINISH_EDITING_CONFIG }; 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/actions/indexes.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | import { ApplicationState, ThunkResult } from '../reducers'; 3 | 4 | export const FETCH_INDEXES_REQUEST = 'FETCH_INDEXES_REQUEST'; 5 | export const FETCH_INDEXES_SUCCESS = 'FETCH_INDEXES_SUCCESS'; 6 | export const FETCH_INDEXES_FAILURE = 'FETCH_INDEXES_FAILURE'; 7 | 8 | export function fetchTableIndexesIfNeeded( 9 | database: string, 10 | table: string, 11 | schema?: string, 12 | ): ThunkResult { 13 | return (dispatch, getState) => { 14 | if (shouldFetchTableIndexes(getState(), database, table)) { 15 | dispatch(fetchTableIndexes(database, table, schema)); 16 | } 17 | }; 18 | } 19 | 20 | function shouldFetchTableIndexes( 21 | state: ApplicationState, 22 | database: string, 23 | table: string, 24 | ): boolean { 25 | const indexes = state.indexes; 26 | if (!indexes) return true; 27 | if (indexes.isFetching) return false; 28 | if (!indexes.indexesByTable[database]) return true; 29 | if (!indexes.indexesByTable[database][table]) return true; 30 | return indexes.didInvalidate; 31 | } 32 | 33 | function fetchTableIndexes(database: string, table: string, schema?: string): ThunkResult { 34 | return async (dispatch) => { 35 | dispatch({ type: FETCH_INDEXES_REQUEST, database, table }); 36 | try { 37 | const indexes = await sqlectron.db.listTableIndexes(database, table, schema); 38 | dispatch({ 39 | type: FETCH_INDEXES_SUCCESS, 40 | database, 41 | table, 42 | indexes, 43 | }); 44 | } catch (error) { 45 | dispatch({ type: FETCH_INDEXES_FAILURE, error }); 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/actions/keys.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | import { ApplicationState, ThunkResult } from '../reducers'; 3 | 4 | export const FETCH_KEYS_REQUEST = 'FETCH_KEYS_REQUEST'; 5 | export const FETCH_KEYS_SUCCESS = 'FETCH_KEYS_SUCCESS'; 6 | export const FETCH_KEYS_FAILURE = 'FETCH_KEYS_FAILURE'; 7 | 8 | export function fetchTableKeysIfNeeded( 9 | database: string, 10 | table: string, 11 | schema?: string, 12 | ): ThunkResult { 13 | return (dispatch, getState) => { 14 | if (shouldFetchTableKeys(getState(), database, table)) { 15 | dispatch(fetchTableKeys(database, table, schema)); 16 | } 17 | }; 18 | } 19 | 20 | function shouldFetchTableKeys(state: ApplicationState, database: string, table: string): boolean { 21 | const keys = state.keys; 22 | if (!keys) return true; 23 | if (keys.isFetching[database] && keys.isFetching[database][table]) return false; 24 | if (!keys.keysByTable[database]) return true; 25 | if (!keys.keysByTable[database][table]) return true; 26 | return keys.didInvalidate; 27 | } 28 | 29 | function fetchTableKeys(database: string, table: string, schema?: string): ThunkResult { 30 | return async (dispatch) => { 31 | dispatch({ type: FETCH_KEYS_REQUEST, database, table }); 32 | try { 33 | const tableKeys = await sqlectron.db.getTableKeys(database, table, schema); 34 | dispatch({ 35 | type: FETCH_KEYS_SUCCESS, 36 | database, 37 | table, 38 | tableKeys, 39 | }); 40 | } catch (error) { 41 | dispatch({ type: FETCH_KEYS_FAILURE, error }); 42 | } 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/actions/routines.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | import { ApplicationState, ThunkResult } from '../reducers'; 3 | import { SchemaFilter } from '../../common/types/database'; 4 | 5 | export const FETCH_ROUTINES_REQUEST = 'FETCH_ROUTINES_REQUEST'; 6 | export const FETCH_ROUTINES_SUCCESS = 'FETCH_ROUTINES_SUCCESS'; 7 | export const FETCH_ROUTINES_FAILURE = 'FETCH_ROUTINES_FAILURE'; 8 | 9 | export function fetchRoutinesIfNeeded(database: string, filter?: SchemaFilter): ThunkResult { 10 | return (dispatch, getState) => { 11 | if (shouldFetchRoutines(getState(), database)) { 12 | dispatch(fetchRoutines(database, filter)); 13 | } 14 | }; 15 | } 16 | 17 | function shouldFetchRoutines(state: ApplicationState, database: string): boolean { 18 | const routines = state.routines; 19 | if (!routines) return true; 20 | if (routines.isFetching) return false; 21 | if (!routines.functionsByDatabase[database]) return true; 22 | if (!routines.proceduresByDatabase[database]) return true; 23 | return routines.didInvalidate; 24 | } 25 | 26 | function fetchRoutines(database: string, filter?: SchemaFilter): ThunkResult { 27 | return async (dispatch) => { 28 | dispatch({ type: FETCH_ROUTINES_REQUEST, database }); 29 | try { 30 | const routines = await sqlectron.db.listRoutines(database, filter); 31 | dispatch({ type: FETCH_ROUTINES_SUCCESS, database, routines }); 32 | } catch (error) { 33 | dispatch({ type: FETCH_ROUTINES_FAILURE, error }); 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/actions/schemas.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | import { ApplicationState, ThunkResult } from '../reducers'; 3 | 4 | export const FETCH_SCHEMAS_REQUEST = 'FETCH_SCHEMAS_REQUEST'; 5 | export const FETCH_SCHEMAS_SUCCESS = 'FETCH_SCHEMAS_SUCCESS'; 6 | export const FETCH_SCHEMAS_FAILURE = 'FETCH_SCHEMAS_FAILURE'; 7 | 8 | export function fetchSchemasIfNeeded(database: string): ThunkResult { 9 | return (dispatch, getState) => { 10 | if (shouldFetchSchemas(getState(), database)) { 11 | dispatch(fetchSchemas(database)); 12 | } 13 | }; 14 | } 15 | 16 | function shouldFetchSchemas(state: ApplicationState, database: string): boolean { 17 | const schemas = state.schemas; 18 | if (!schemas) return true; 19 | if (schemas.isFetching) return false; 20 | if (!schemas.itemsByDatabase[database]) return true; 21 | return schemas.didInvalidate; 22 | } 23 | 24 | function fetchSchemas(database: string): ThunkResult { 25 | return async (dispatch) => { 26 | dispatch({ type: FETCH_SCHEMAS_REQUEST, database }); 27 | try { 28 | // TODO: pass real filter setting 29 | const schemas = await sqlectron.db.listSchemas(database, {}); 30 | dispatch({ type: FETCH_SCHEMAS_SUCCESS, database, schemas }); 31 | } catch (error) { 32 | dispatch({ type: FETCH_SCHEMAS_FAILURE, error }); 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/actions/tables.ts: -------------------------------------------------------------------------------- 1 | import { AnyAction } from 'redux'; 2 | import { sqlectron } from '../api'; 3 | import { ApplicationState, ThunkResult } from '../reducers'; 4 | import { SchemaFilter } from '../../common/types/database'; 5 | 6 | export const FETCH_TABLES_REQUEST = 'FETCH_TABLES_REQUEST'; 7 | export const FETCH_TABLES_SUCCESS = 'FETCH_TABLES_SUCCESS'; 8 | export const FETCH_TABLES_FAILURE = 'FETCH_TABLES_FAILURE'; 9 | export const SELECT_TABLES_FOR_DIAGRAM = 'SELECT_TABLES_FOR_DIAGRAM'; 10 | 11 | export function selectTablesForDiagram(tables: Array): AnyAction { 12 | return { type: SELECT_TABLES_FOR_DIAGRAM, tables }; 13 | } 14 | 15 | export function fetchTablesIfNeeded(database: string, filter?: SchemaFilter): ThunkResult { 16 | return (dispatch, getState) => { 17 | if (shouldFetchTables(getState(), database)) { 18 | dispatch(fetchTables(database, filter)); 19 | } 20 | }; 21 | } 22 | 23 | function shouldFetchTables(state: ApplicationState, database: string): boolean { 24 | const tables = state.tables; 25 | if (!tables) return true; 26 | if (tables.isFetching) return false; 27 | if (!tables.itemsByDatabase[database]) return true; 28 | return tables.didInvalidate; 29 | } 30 | 31 | function fetchTables(database: string, filter?: SchemaFilter): ThunkResult { 32 | return async (dispatch) => { 33 | dispatch({ type: FETCH_TABLES_REQUEST, database }); 34 | try { 35 | const tables = await sqlectron.db.listTables(database, filter); 36 | dispatch({ type: FETCH_TABLES_SUCCESS, database, tables }); 37 | } catch (error) { 38 | dispatch({ type: FETCH_TABLES_FAILURE, error }); 39 | } 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/actions/triggers.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | import { ApplicationState, ThunkResult } from '../reducers'; 3 | 4 | export const FETCH_TRIGGERS_REQUEST = 'FETCH_TRIGGERS_REQUEST'; 5 | export const FETCH_TRIGGERS_SUCCESS = 'FETCH_TRIGGERS_SUCCESS'; 6 | export const FETCH_TRIGGERS_FAILURE = 'FETCH_TRIGGERS_FAILURE'; 7 | 8 | export function fetchTableTriggersIfNeeded( 9 | database: string, 10 | table: string, 11 | schema?: string, 12 | ): ThunkResult { 13 | return (dispatch, getState) => { 14 | if (shouldFetchTableTriggers(getState(), database, table)) { 15 | dispatch(fetchTableTriggers(database, table, schema)); 16 | } 17 | }; 18 | } 19 | 20 | function shouldFetchTableTriggers( 21 | state: ApplicationState, 22 | database: string, 23 | table: string, 24 | ): boolean { 25 | const triggers = state.triggers; 26 | if (!triggers) return true; 27 | if (triggers.isFetching) return false; 28 | if (!triggers.triggersByTable[database]) return true; 29 | if (!triggers.triggersByTable[database][table]) return true; 30 | return triggers.didInvalidate; 31 | } 32 | 33 | function fetchTableTriggers(database: string, table: string, schema?: string): ThunkResult { 34 | return async (dispatch) => { 35 | dispatch({ type: FETCH_TRIGGERS_REQUEST, database, table }); 36 | try { 37 | const triggers = await sqlectron.db.listTableTriggers(database, table, schema); 38 | dispatch({ 39 | type: FETCH_TRIGGERS_SUCCESS, 40 | database, 41 | table, 42 | triggers, 43 | }); 44 | } catch (error) { 45 | dispatch({ type: FETCH_TRIGGERS_FAILURE, error }); 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/actions/views.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | import { ApplicationState, ThunkResult } from '../reducers'; 3 | import { SchemaFilter } from '../../common/types/database'; 4 | 5 | export const FETCH_VIEWS_REQUEST = 'FETCH_VIEWS_REQUEST'; 6 | export const FETCH_VIEWS_SUCCESS = 'FETCH_VIEWS_SUCCESS'; 7 | export const FETCH_VIEWS_FAILURE = 'FETCH_VIEWS_FAILURE'; 8 | 9 | export function fetchViewsIfNeeded(database: string, filter?: SchemaFilter): ThunkResult { 10 | return (dispatch, getState) => { 11 | if (shouldFetchViews(getState(), database)) { 12 | dispatch(fetchViews(database, filter)); 13 | } 14 | }; 15 | } 16 | 17 | function shouldFetchViews(state: ApplicationState, database: string): boolean { 18 | const views = state.views; 19 | if (!views) return true; 20 | if (views.isFetching) return false; 21 | if (!views.viewsByDatabase[database]) return true; 22 | return views.didInvalidate; 23 | } 24 | 25 | function fetchViews(database: string, filter?: SchemaFilter): ThunkResult { 26 | return async (dispatch) => { 27 | dispatch({ type: FETCH_VIEWS_REQUEST, database }); 28 | try { 29 | const views = await sqlectron.db.listViews(database, filter); 30 | dispatch({ type: FETCH_VIEWS_SUCCESS, database, views }); 31 | } catch (error) { 32 | dispatch({ type: FETCH_VIEWS_FAILURE, error }); 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/api.ts: -------------------------------------------------------------------------------- 1 | import { SqlectronAPI } from '../common/types/api'; 2 | 3 | declare global { 4 | interface Window { 5 | sqlectron: SqlectronAPI; 6 | } 7 | } 8 | 9 | // Export sqlectron API with the integration for any backend core logic call 10 | export const sqlectron = window.sqlectron; 11 | 12 | // Load metadata configuration only once and export them as constants 13 | export const CONFIG = sqlectron.config.getFullSync(); 14 | export const DB_CLIENTS = sqlectron.db.getClientsSync(); 15 | -------------------------------------------------------------------------------- /src/renderer/assets/server-db-client/cassandra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/assets/server-db-client/cassandra.png -------------------------------------------------------------------------------- /src/renderer/assets/server-db-client/mariadb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/assets/server-db-client/mariadb.png -------------------------------------------------------------------------------- /src/renderer/assets/server-db-client/mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/assets/server-db-client/mysql.png -------------------------------------------------------------------------------- /src/renderer/assets/server-db-client/postgresql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/assets/server-db-client/postgresql.png -------------------------------------------------------------------------------- /src/renderer/assets/server-db-client/redshift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/assets/server-db-client/redshift.png -------------------------------------------------------------------------------- /src/renderer/assets/server-db-client/sqlite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/assets/server-db-client/sqlite.png -------------------------------------------------------------------------------- /src/renderer/assets/server-db-client/sqlserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/assets/server-db-client/sqlserver.png -------------------------------------------------------------------------------- /src/renderer/components/_breakpoint.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Settings breakpoint mixin 3 | */ 4 | @import "~breakpoint-sass"; 5 | @include add-breakpoint('bigger than x-small', 512px); 6 | @include add-breakpoint('bigger than small', 600px); 7 | @include add-breakpoint('bigger than small-medium', 850px); 8 | @include add-breakpoint('bigger than medium', 992px); 9 | @include add-breakpoint('between small and medium', 768px 992px); 10 | @include add-breakpoint('less than small', max-width 768px); 11 | @include add-breakpoint('less than medium', max-width 992px); 12 | -------------------------------------------------------------------------------- /src/renderer/components/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, FC, useCallback } from 'react'; 2 | 3 | export interface CheckboxProps { 4 | name: string; 5 | label: string; 6 | disabled?: boolean; 7 | checked: boolean; 8 | onChecked: () => void; 9 | onUnchecked: () => void; 10 | } 11 | 12 | const Checkbox: FC = ({ 13 | name, 14 | label, 15 | disabled, 16 | checked, 17 | onChecked, 18 | onUnchecked, 19 | }) => { 20 | const handleChange = useCallback( 21 | (e: ChangeEvent) => { 22 | if (e.target.checked) { 23 | onChecked(); 24 | } else { 25 | onUnchecked(); 26 | } 27 | }, 28 | [onChecked, onUnchecked], 29 | ); 30 | 31 | return ( 32 |
33 | 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default Checkbox; 46 | -------------------------------------------------------------------------------- /src/renderer/components/collapse-icon.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | interface CollapseIconProps { 4 | arrowDirection: 'down' | 'right'; 5 | expandAction?: () => void; 6 | } 7 | 8 | const CollapseIcon: FC = ({ arrowDirection, expandAction }) => ( 9 | 14 | ); 15 | 16 | export default CollapseIcon; 17 | -------------------------------------------------------------------------------- /src/renderer/components/confim-modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | 3 | interface ConfirmModalProps { 4 | title: string; 5 | message: string; 6 | context: string; 7 | onCancelClick: () => void; 8 | onRemoveClick: () => void; 9 | } 10 | 11 | const ConfirmModal = ({ 12 | onCancelClick, 13 | onRemoveClick, 14 | title, 15 | message, 16 | context, 17 | }: ConfirmModalProps) => { 18 | const ref = useRef(null); 19 | 20 | useEffect(() => { 21 | if (!ref.current) { 22 | return; 23 | } 24 | const elem = ref.current; 25 | $(elem) 26 | .modal({ 27 | closable: false, 28 | detachable: false, 29 | allowMultiple: true, 30 | context: context, 31 | onDeny: () => { 32 | onCancelClick(); 33 | }, 34 | onApprove: () => { 35 | onRemoveClick(); 36 | }, 37 | }) 38 | .modal('show'); 39 | return () => { 40 | $(elem).modal('hide'); 41 | }; 42 | }, [ref, context, onCancelClick, onRemoveClick]); 43 | 44 | return ( 45 |
46 |
{title}
47 |
{message}
48 |
49 |
50 | No 51 | 52 |
53 |
54 | Yes 55 | 56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | export default ConfirmModal; 63 | -------------------------------------------------------------------------------- /src/renderer/components/database-diagram-shapes.js: -------------------------------------------------------------------------------- 1 | import * as joint from 'jointjs'; 2 | import { bindAll, template } from 'lodash'; 3 | 4 | // Custom joint shape representing table/view object 5 | const SqlectronShapes = {}; 6 | 7 | // Custom joint shape representing table/view object 8 | SqlectronShapes.Table = joint.shapes.basic.Rect.extend({ 9 | defaults: joint.util.deepSupplement( 10 | { 11 | type: 'sqlectron.Table', 12 | attrs: { 13 | rect: { stroke: 'none', 'fill-opacity': 0, fill: 'red' }, 14 | }, 15 | }, 16 | joint.shapes.basic.Rect.prototype.defaults, 17 | ), 18 | }); 19 | 20 | SqlectronShapes.TableView = joint.dia.ElementView.extend({ 21 | template: '

', 22 | 23 | initialize(...args) { 24 | bindAll(this, 'updateBox'); 25 | joint.dia.ElementView.prototype.initialize.apply(this, args); 26 | this.$box = $(template(this.template)()); 27 | 28 | this.$box.find('span').text(this.model.get('name')); 29 | this.$box.addClass(this.model.get('name')); 30 | 31 | // Update the box position whenever the underlying model changes. 32 | this.model.on('change', this.updateBox, this); 33 | 34 | this.updateBox(); 35 | }, 36 | render(...args) { 37 | joint.dia.ElementView.prototype.render.apply(this, args); 38 | this.paper.$el.prepend(this.$box); 39 | return this; 40 | }, 41 | updateBox() { 42 | // Set the position and dimension of the box so that it covers the JointJS element. 43 | const bbox = this.model.getBBox(); 44 | this.$box.css({ 45 | width: bbox.width, 46 | height: bbox.height, 47 | left: bbox.x, 48 | top: bbox.y, 49 | transform: `rotate(${this.model.get('angle') || 0}deg)`, 50 | }); 51 | }, 52 | }); 53 | 54 | SqlectronShapes.TableCell = joint.shapes.basic.Rect.extend({ 55 | defaults: joint.util.deepSupplement( 56 | { 57 | type: 'sqlectron.TableCell', 58 | attrs: { 59 | rect: { stroke: 'none', 'fill-opacity': 0, style: { 'pointer-events': 'none' } }, 60 | }, 61 | }, 62 | joint.shapes.basic.Rect.prototype.defaults, 63 | ), 64 | }); 65 | 66 | SqlectronShapes.TableCellView = joint.dia.ElementView.extend({ 67 | template: '
', 68 | 69 | initialize(...args) { 70 | bindAll(this, 'updateCell'); 71 | joint.dia.ElementView.prototype.initialize.apply(this, args); 72 | this.$box = $(template(this.template)()); 73 | 74 | const keyType = this.model.get('keyType'); 75 | const keyColor = keyType === 'PRIMARY KEY' ? 'yellow' : ''; 76 | const cellSpanEl = this.$box.find('span'); 77 | 78 | cellSpanEl.text(this.model.get('name')); 79 | this.$box.addClass(this.model.get('tableName')); 80 | 81 | if (keyType) { 82 | cellSpanEl.prepend(``); 83 | } else { 84 | cellSpanEl.css({ 85 | paddingLeft: '1.18em', 86 | marginLeft: '0.25rem', 87 | }); 88 | } 89 | 90 | this.model.on('change', this.updateCell, this); 91 | this.updateCell(); 92 | }, 93 | render(...args) { 94 | joint.dia.ElementView.prototype.render.apply(this, args); 95 | this.paper.$el.prepend(this.$box); 96 | return this; 97 | }, 98 | updateCell() { 99 | const bbox = this.model.getBBox(); 100 | this.$box.css({ 101 | width: bbox.width, 102 | height: bbox.height, 103 | left: bbox.x, 104 | top: bbox.y, 105 | transform: `rotate(${this.model.get('angle') || 0}deg)`, 106 | }); 107 | }, 108 | }); 109 | 110 | const cellNamespace = { sqlectron: SqlectronShapes }; 111 | 112 | export { joint, SqlectronShapes, cellNamespace }; 113 | -------------------------------------------------------------------------------- /src/renderer/components/database-diagram.css: -------------------------------------------------------------------------------- 1 | /* Disable removing links between objects */ 2 | .link-tools .tool-remove { 3 | display: none; 4 | } 5 | 6 | /* Make custom element appear above jointjs generic rect */ 7 | .sqlectron-table, 8 | .sqlectron-table-cell { 9 | position: absolute; 10 | /* Make sure events are propagated to the JointJS element so, e.g. dragging works.*/ 11 | pointer-events: none; 12 | -webkit-user-select: none; 13 | z-index: 2; 14 | } 15 | 16 | .sqlectron-table { 17 | border-radius: 4px; 18 | border: 2px solid; 19 | box-shadow: inset 0 0 1px black, 1px 1px 1px gray; 20 | padding: 5px; 21 | } 22 | 23 | .sqlectron-table p { 24 | font-size: 14px; 25 | font-weight: bold; 26 | text-align: center; 27 | background-color: rgba(34, 36, 38, 0.15); 28 | } 29 | 30 | .sqlectron-table-cell { 31 | margin: 0 7px 0 7px; 32 | } 33 | 34 | /* Disable disconnecting links from elements */ 35 | .marker-arrowheads { 36 | display: none; 37 | } 38 | 39 | .ui.simple.upward.dropdown > .menu { 40 | top: auto !important; 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/components/database-filter.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, forwardRef, useCallback } from 'react'; 2 | 3 | interface Props { 4 | value?: string; 5 | placeholder?: string; 6 | isFetching: boolean; 7 | onFilterChange: (value: string) => void; 8 | onFocus?: () => void; 9 | } 10 | 11 | const DatabaseFilter = forwardRef( 12 | ({ value, placeholder, isFetching, onFilterChange, onFocus }, ref) => { 13 | const handleFilterChange = useCallback( 14 | (event: ChangeEvent): void => { 15 | onFilterChange(event.target.value); 16 | }, 17 | [onFilterChange], 18 | ); 19 | 20 | return ( 21 |
22 | 31 | 32 |
33 | ); 34 | }, 35 | ); 36 | 37 | DatabaseFilter.displayName = 'DatabaseFilter'; 38 | 39 | export default DatabaseFilter; 40 | -------------------------------------------------------------------------------- /src/renderer/components/database-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, RefObject } from 'react'; 2 | import DatabaseListItem from './database-list-item'; 3 | import { Database } from '../reducers/databases'; 4 | import { DbTable } from '../../common/types/database'; 5 | import type { ActionType, ObjectType } from '../reducers/sqlscripts'; 6 | 7 | interface Props { 8 | client: string; 9 | databases: Database[]; 10 | databaseRefs: Record>; 11 | currentDB: string | null; 12 | isFetching: boolean; 13 | onSelectDatabase: (database: Database) => void; 14 | onExecuteDefaultQuery: (database: Database, table: DbTable) => void; 15 | onSelectTable: (database: Database, table: DbTable) => void; 16 | onGetSQLScript: ( 17 | database: Database, 18 | item: { name: string; schema?: string }, 19 | actionType: ActionType, 20 | objectType: ObjectType, 21 | ) => void; 22 | onRefreshDatabase: (database: Database) => void; 23 | onOpenTab: (database: Database) => void; 24 | onShowDiagramModal: (database: Database) => void; 25 | } 26 | 27 | const DatabaseList: FC = ({ 28 | client, 29 | databases, 30 | databaseRefs, 31 | currentDB, 32 | isFetching, 33 | onSelectDatabase, 34 | onExecuteDefaultQuery, 35 | onSelectTable, 36 | onGetSQLScript, 37 | onRefreshDatabase, 38 | onOpenTab, 39 | onShowDiagramModal, 40 | }) => { 41 | if (isFetching) { 42 | return
Loading...
; 43 | } 44 | 45 | if (!databases.length) { 46 | return
No results found
; 47 | } 48 | 49 | return ( 50 |
51 | {databases.map((database) => ( 52 | 66 | ))} 67 |
68 | ); 69 | }; 70 | 71 | DatabaseList.displayName = 'DatabaseList'; 72 | export default DatabaseList; 73 | -------------------------------------------------------------------------------- /src/renderer/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, MouseEvent } from 'react'; 2 | 3 | import { sqlectron } from '../api'; 4 | import UpdateChecker from './update-checker'; 5 | import LogStatus from './log-status'; 6 | 7 | interface Props { 8 | status: string; 9 | } 10 | 11 | const Footer: FC = ({ status }) => { 12 | const onGithubClick = (event: MouseEvent) => { 13 | event.preventDefault(); 14 | sqlectron.browser.shell.openExternal('https://github.com/sqlectron/sqlectron-gui'); 15 | }; 16 | 17 | const onShortcutsClick = (event: MouseEvent) => { 18 | event.preventDefault(); 19 | sqlectron.browser.shell.openExternal( 20 | 'https://github.com/sqlectron/sqlectron-gui/wiki/Keyboard-Shortcuts', 21 | ); 22 | }; 23 | 24 | return ( 25 |
26 |
{status}
27 |
28 |
29 | 30 | 31 |
32 | 33 | GitHub 34 | 35 | 36 | 37 | 38 |
39 |
40 | ); 41 | }; 42 | 43 | Footer.displayName = 'Footer'; 44 | 45 | export default Footer; 46 | -------------------------------------------------------------------------------- /src/renderer/components/header.css: -------------------------------------------------------------------------------- 1 | #header .right.menu { 2 | margin-left: 0 !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/renderer/components/header.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { sqlectron } from '../api'; 3 | 4 | import LOGO_PATH from './logo-128px.png'; 5 | 6 | require('./header.css'); 7 | 8 | function onSiteClick(event) { 9 | event.preventDefault(); 10 | sqlectron.browser.shell.openExternal('https://sqlectron.github.io'); 11 | } 12 | 13 | function renderBreadcrumb(items) { 14 | return ( 15 |
16 | {items.map(({ icon, label }, index) => { 17 | const isLast = index !== items.length - 1; 18 | return ( 19 | 20 | 21 | {label} 22 | {isLast &&
/
} 23 |
24 | ); 25 | })} 26 |
27 | ); 28 | } 29 | 30 | interface Props { 31 | items: { icon: string; label: string }[]; 32 | onCloseConnectionClick?: () => void; 33 | onReConnectionClick?: () => void; 34 | } 35 | 36 | const Header: FC = ({ items, onCloseConnectionClick, onReConnectionClick }) => { 37 | const visibilityButtons = onCloseConnectionClick ? 'visible' : 'hidden'; 38 | const styleItem = { paddingLeft: 0, paddingTop: 0, paddingBottom: 0 }; 39 | return ( 40 | 65 | ); 66 | }; 67 | 68 | Header.displayName = 'Header'; 69 | 70 | export default Header; 71 | -------------------------------------------------------------------------------- /src/renderer/components/loader.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useRef } from 'react'; 2 | 3 | interface Props { 4 | message?: string; 5 | type: 'active' | 'page'; 6 | inverted?: boolean; 7 | } 8 | 9 | const Loader: FC = ({ message, type, inverted = false }) => { 10 | const ref = useRef(null); 11 | 12 | useEffect(() => { 13 | if (!ref.current) { 14 | return; 15 | } 16 | const elem = ref.current; 17 | $(elem).dimmer('show'); 18 | return () => { 19 | $(elem).dimmer('hide'); 20 | }; 21 | }, [ref]); 22 | 23 | return ( 24 |
25 |
{message}
26 |
27 | ); 28 | }; 29 | 30 | Loader.displayName = 'Loader'; 31 | 32 | export default Loader; 33 | -------------------------------------------------------------------------------- /src/renderer/components/log-status.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { CONFIG } from '../api'; 3 | 4 | const log = CONFIG.log; 5 | 6 | const LogStatus: FC = () => { 7 | if (!log.console && !log.file) { 8 | return null; 9 | } 10 | 11 | return ( 12 | 13 | 14 | Log 15 |
{log.level}
16 |
17 | ); 18 | }; 19 | 20 | export default LogStatus; 21 | -------------------------------------------------------------------------------- /src/renderer/components/logo-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/components/logo-128px.png -------------------------------------------------------------------------------- /src/renderer/components/message.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useCallback, useRef } from 'react'; 2 | 3 | interface Props { 4 | closeable?: boolean; 5 | type?: string; 6 | title?: string; 7 | message?: string; 8 | preformatted?: boolean; 9 | } 10 | 11 | const Message: FC = ({ closeable, type, title, message, preformatted }) => { 12 | const ref = useRef(null); 13 | const onClose = useCallback(() => { 14 | if (ref.current) { 15 | $(ref.current).transition('fade'); 16 | } 17 | }, [ref]); 18 | 19 | return ( 20 |
21 | {closeable && } 22 | {title &&
{title}
} 23 | {message && preformatted ?
{message}
:

{message}

} 24 |
25 | ); 26 | }; 27 | 28 | Message.displayName = 'Message'; 29 | 30 | export default Message; 31 | -------------------------------------------------------------------------------- /src/renderer/components/override-ace.css: -------------------------------------------------------------------------------- 1 | .ace_editor.ace_autocomplete .ace_completion-highlight { 2 | /* Avoid Blurry render of Highlighting in Retina display */ 3 | text-shadow: 1px 0px 0px !important; 4 | } 5 | -------------------------------------------------------------------------------- /src/renderer/components/preview-modal.tsx: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash/isPlainObject'; 2 | import React, { FC, useCallback, useEffect, useRef, useState } from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | interface Props { 6 | value: unknown; 7 | onCloseClick: () => void; 8 | } 9 | 10 | const PreviewModal: FC = ({ value, onCloseClick }) => { 11 | const modalRef = useRef(null); 12 | 13 | const [selected, setSelected] = useState(null); 14 | 15 | useEffect(() => { 16 | if (!modalRef.current) { 17 | return; 18 | } 19 | const elem = modalRef.current; 20 | $(elem) 21 | .modal({ 22 | context: 'body', 23 | closable: false, 24 | detachable: false, 25 | onDeny: () => { 26 | onCloseClick(); 27 | }, 28 | }) 29 | .modal('show'); 30 | return () => { 31 | $(elem).modal('hide'); 32 | }; 33 | }, [modalRef, onCloseClick]); 34 | 35 | const getPreviewValue = useCallback( 36 | (type: string | null) => { 37 | try { 38 | switch (type) { 39 | case 'plain': 40 | return isPlainObject(value) ? JSON.stringify(value) : (value as string); 41 | case 'json': 42 | return
{JSON.stringify(value, null, 2)}
; 43 | default: 44 | return value as string; 45 | } 46 | } catch (err) { 47 | return 'Not valid format'; 48 | } 49 | }, 50 | [value], 51 | ); 52 | 53 | const onClick = useCallback((type) => setSelected(type), []); 54 | 55 | const items = [ 56 | { type: 'plain', name: 'Plain Text', default: true }, 57 | { type: 'json', name: 'JSON' }, 58 | ]; 59 | 60 | return ( 61 |
62 |
Content Preview
63 |
64 |
65 | {items.map((item) => { 66 | const className = classNames({ 67 | item: true, 68 | active: (!selected && item.default) || selected === item.type, 69 | }); 70 | 71 | return ( 72 | onClick(item.type)} className={className}> 73 | {item.name} 74 | 75 | ); 76 | })} 77 |
78 |
79 |
80 | {getPreviewValue(selected || 'plain')} 81 |
82 |
83 |
84 |
85 |
86 | Close 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | PreviewModal.displayName = 'PreviewModal'; 94 | export default PreviewModal; 95 | -------------------------------------------------------------------------------- /src/renderer/components/prompt-modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ChangeEvent, 3 | FC, 4 | KeyboardEvent, 5 | useCallback, 6 | useEffect, 7 | useRef, 8 | useState, 9 | } from 'react'; 10 | 11 | interface Props { 12 | onCancelClick: () => void; 13 | onOKClick: (value: string) => void; 14 | title: string; 15 | message: string; 16 | type: string; 17 | } 18 | 19 | const PromptModal: FC = ({ onCancelClick, onOKClick, title, message, type }) => { 20 | const ref = useRef(null); 21 | 22 | const [value, setValue] = useState(''); 23 | 24 | useEffect(() => { 25 | if (!ref.current) { 26 | return; 27 | } 28 | const elem = ref.current; 29 | $(elem).modal({ 30 | closable: false, 31 | detachable: false, 32 | onDeny: () => { 33 | onCancelClick(); 34 | }, 35 | onApprove: () => { 36 | onOKClick(value); 37 | }, 38 | }); 39 | }, [ref, onCancelClick, onOKClick, value]); 40 | 41 | useEffect(() => { 42 | if (!ref.current) { 43 | return; 44 | } 45 | const elem = ref.current; 46 | $(elem).modal('show'); 47 | return () => { 48 | $(elem).modal('hide'); 49 | }; 50 | }, [ref]); 51 | 52 | const handleKeyPress = useCallback( 53 | (event: KeyboardEvent): void => { 54 | if (event.key === 'Enter') { 55 | onOKClick(value); 56 | } 57 | }, 58 | [onOKClick, value], 59 | ); 60 | 61 | const handleChange = useCallback((event: ChangeEvent): void => { 62 | setValue(event?.target.value); 63 | }, []); 64 | 65 | return ( 66 |
67 |
{title}
68 |
69 | {message} 70 |
71 | 72 |
73 |
74 |
75 |
76 | Cancel 77 | 78 |
79 |
80 | OK 81 | 82 |
83 |
84 |
85 | ); 86 | }; 87 | 88 | PromptModal.displayName = 'PromptModal'; 89 | export default PromptModal; 90 | -------------------------------------------------------------------------------- /src/renderer/components/query-result-table-cell.tsx: -------------------------------------------------------------------------------- 1 | import isPlainObject from 'lodash/isPlainObject'; 2 | import classNames from 'classnames'; 3 | import React, { FC, MouseEvent, useCallback, useEffect, useState } from 'react'; 4 | import ContextMenu from '../utils/context-menu'; 5 | import * as eventKeys from '../../common/event'; 6 | import { valueToString } from '../../common/utils/convert'; 7 | 8 | const MENU_CTX_ID = 'CONTEXT_MENU_TABLE_CELL'; 9 | 10 | interface Props { 11 | rowIndex: number; 12 | col: string; 13 | data: any; 14 | onOpenPreviewClick: (value: any) => void; 15 | } 16 | 17 | const QueryResultTableCell: FC = ({ rowIndex, col, data, onOpenPreviewClick }) => { 18 | const [contextMenu, setContextMenu] = useState(null); 19 | const [showMenuEvent, setShowMenuEvent] = useState(null); 20 | 21 | useEffect(() => { 22 | if (contextMenu) { 23 | return () => { 24 | contextMenu.dispose(); 25 | }; 26 | } 27 | }, [contextMenu]); 28 | 29 | const getValue = useCallback(() => { 30 | return data[rowIndex][col]; 31 | }, [data, col, rowIndex]); 32 | 33 | const onContextMenu = useCallback( 34 | (event: MouseEvent) => { 35 | event.preventDefault(); 36 | 37 | const value = getValue(); 38 | 39 | const hasPreview = typeof value === 'string' || isPlainObject(value); 40 | 41 | if (!contextMenu && hasPreview) { 42 | const newContextMenu = new ContextMenu(MENU_CTX_ID); 43 | 44 | newContextMenu.append({ 45 | label: 'Open Preview', 46 | event: eventKeys.BROWSER_MENU_OPEN_PREVIEW, 47 | click: () => onOpenPreviewClick(value), 48 | }); 49 | 50 | newContextMenu.build(); 51 | setContextMenu(newContextMenu); 52 | } 53 | event.persist(); 54 | setShowMenuEvent(event); 55 | }, 56 | [contextMenu, getValue, onOpenPreviewClick], 57 | ); 58 | 59 | useEffect(() => { 60 | if (showMenuEvent && contextMenu) { 61 | contextMenu.popup({ 62 | x: showMenuEvent.clientX, 63 | y: showMenuEvent.clientY, 64 | }); 65 | setShowMenuEvent(null); 66 | } 67 | }, [contextMenu, showMenuEvent]); 68 | 69 | const value = getValue(); 70 | const className = classNames({ 71 | 'ui mini grey label table-cell-type-null': value === null, 72 | }); 73 | 74 | return ( 75 |
76 | {value === null ? NULL : valueToString(value)} 77 |
78 | ); 79 | }; 80 | 81 | QueryResultTableCell.displayName = 'QueryResultTableCell'; 82 | export default QueryResultTableCell; 83 | -------------------------------------------------------------------------------- /src/renderer/components/query-result-table.scss: -------------------------------------------------------------------------------- 1 | .grid-query-wrapper { 2 | border-color: #d3d3d3; 3 | border-style: solid; 4 | border-width: 1px; 5 | box-sizing: border-box; 6 | border-radius: .25rem; 7 | } 8 | 9 | .ReactVirtualized__Grid { 10 | outline: none; 11 | } 12 | 13 | .ReactVirtualized__Grid.grid-header-row .ReactVirtualized__Grid__cell .draggable-handle { 14 | width: 5px; 15 | cursor: col-resize; 16 | height: 30px; 17 | position: absolute; 18 | right: 0; 19 | border-right: 1px solid #bfbfbf; 20 | top: 0; 21 | z-index: 10000; 22 | } 23 | 24 | .ReactVirtualized__Grid.grid-header-row .ReactVirtualized__Grid__cell .draggable-handle:hover, 25 | .ReactVirtualized__Grid.grid-header-row .ReactVirtualized__Grid__cell .react-draggable-dragging { 26 | border-right: 3px solid #0284ff; 27 | } 28 | 29 | .ReactVirtualized__Grid.grid-body { 30 | background: rgb(245, 245, 245); 31 | overflow: hidden !important; 32 | } 33 | 34 | .ReactVirtualized__Grid.grid-body ::-webkit-scrollbar{ display: none } 35 | .ReactVirtualized__Grid.grid-body:hover { overflow:auto !important; } 36 | .ReactVirtualized__Grid.grid-body:hover ::-webkit-scrollbar { display: block } 37 | 38 | .ReactVirtualized__Grid.grid-body > .ReactVirtualized__Grid__innerScrollContainer { 39 | background: #fff; 40 | } 41 | 42 | .ReactVirtualized__Grid__cell { 43 | overflow: hidden; 44 | } 45 | 46 | .ReactVirtualized__Grid__cell > .item { 47 | border: 1px solid #eee; 48 | padding-left: 3px; 49 | text-overflow: ellipsis; 50 | overflow: hidden; 51 | white-space: nowrap; 52 | } 53 | 54 | .ReactVirtualized__Grid.grid-header-row .ReactVirtualized__Grid__cell { 55 | overflow: visible; 56 | font-weight: bold; 57 | padding-left: 10px; 58 | padding-right: 10px; 59 | } 60 | 61 | .ReactVirtualized__Grid.grid-header-row .ReactVirtualized__Grid__cell span { 62 | overflow: hidden; 63 | width: 100%; 64 | text-overflow: ellipsis; 65 | display: block; 66 | } 67 | 68 | .ReactVirtualized__Grid.grid-header-row .ReactVirtualized__Grid__cell > .item { 69 | border: none; 70 | } 71 | 72 | .ReactVirtualized__Grid.grid-header-row { 73 | overflow: hidden !important; 74 | background: #f9fafb; 75 | border: 1px solid rgba(34,36,38,.1); 76 | border-right: none; 77 | } 78 | 79 | .grid-header-row .ReactVirtualized__Grid__cell > .item { 80 | border: 1px solid #dadada; 81 | } 82 | 83 | .ReactVirtualized__Grid .table-cell-type-null { 84 | vertical-align: text-bottom; 85 | } 86 | -------------------------------------------------------------------------------- /src/renderer/components/query-result.tsx: -------------------------------------------------------------------------------- 1 | import { groupBy } from 'lodash'; 2 | import React, { FC, ReactElement } from 'react'; 3 | import Message from './message'; 4 | import QueryResultTable from './query-result-table'; 5 | 6 | interface Props { 7 | fields: any[]; 8 | rows: any[]; 9 | rowCount: number | undefined; 10 | affectedRows: number | undefined; 11 | queryIndex: number; 12 | totalQueries: number; 13 | command: string; 14 | isMultipleResults: boolean; 15 | widthOffset: number; 16 | heightOffset: number; 17 | onCopyToClipboardClick: (rows, type: string, delimiter?: string) => void; 18 | onSaveToFileClick: (rows, type: string, delimiter?: string) => void; 19 | copied: boolean | null; 20 | saved: boolean | null; 21 | } 22 | 23 | const QueryResult: FC = ({ 24 | fields, 25 | rows, 26 | rowCount, 27 | affectedRows, 28 | queryIndex, 29 | totalQueries, 30 | command, 31 | isMultipleResults, 32 | widthOffset, 33 | heightOffset, 34 | onCopyToClipboardClick, 35 | onSaveToFileClick, 36 | copied, 37 | saved, 38 | }) => { 39 | const isSelect = command === 'SELECT'; 40 | const isExplain = command === 'EXPLAIN'; 41 | const isUnknown = command === 'UNKNOWN'; 42 | if (!isSelect && !isExplain && !isUnknown) { 43 | const msgAffectedRows = affectedRows ? `Affected rows: ${affectedRows}.` : ''; 44 | return ( 45 | 50 | ); 51 | } 52 | 53 | if (isExplain) { 54 | const title = fields[0].name; 55 | return ( 56 | row[title]).join('\n')} 61 | /> 62 | ); 63 | } 64 | 65 | // Not sure what type of query they ran, but cannot render table, print 66 | // generic message. 67 | if (fields.length === 0) { 68 | return ( 69 | 74 | ); 75 | } 76 | 77 | let msgDuplicatedColumns: null | ReactElement = null; 78 | const groupFields = groupBy(fields, (field) => field.name); 79 | const duplicatedColumns = Object.keys(groupFields).filter( 80 | (field) => groupFields[field].length > 1, 81 | ); 82 | if (duplicatedColumns.length) { 83 | msgDuplicatedColumns = ( 84 | 93 | ); 94 | } 95 | 96 | let adjustedWidthOffset = widthOffset; 97 | if (isMultipleResults) { 98 | adjustedWidthOffset += 30; // padding of the query result box 99 | } 100 | 101 | const tableResult = ( 102 | 114 | ); 115 | 116 | if (totalQueries === 1) { 117 | return ( 118 |
119 | {msgDuplicatedColumns} 120 | {tableResult} 121 |
122 | ); 123 | } 124 | 125 | return ( 126 |
127 |
Query {queryIndex + 1}
128 | {msgDuplicatedColumns} 129 | {tableResult} 130 |
131 | ); 132 | }; 133 | 134 | QueryResult.displayName = 'QueryResult'; 135 | export default QueryResult; 136 | -------------------------------------------------------------------------------- /src/renderer/components/query-results.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import Loader from './loader'; 3 | import Message from './message'; 4 | import QueryResult from './query-result'; 5 | 6 | interface Props { 7 | widthOffset: number; 8 | heightOffset: number; 9 | onCopyToClipboardClick: (rows, type: string, delimiter?: string) => void; 10 | onSaveToFileClick: (rows, type: string, delimiter?: string) => void; 11 | copied: boolean | null; 12 | saved: boolean | null; 13 | query: string | undefined; 14 | results: 15 | | { 16 | command: string; 17 | fields: any[]; 18 | rows: any[]; 19 | rowCount: number | undefined; 20 | affectedRows: number | undefined; 21 | }[] 22 | | null; 23 | isExecuting: boolean; 24 | error: Error | null; 25 | } 26 | 27 | const QueryResults: FC = ({ 28 | widthOffset, 29 | heightOffset, 30 | onCopyToClipboardClick, 31 | onSaveToFileClick, 32 | copied, 33 | saved, 34 | results, 35 | isExecuting, 36 | error, 37 | }) => { 38 | if (error) { 39 | if (error.message) { 40 | const errorBody = Object.keys(error) 41 | .filter((key) => error[key] && key !== 'message') 42 | .map((key) => `${key}: ${error[key]}`) 43 | .join('\n'); 44 | 45 | return ; 46 | } 47 | return
{JSON.stringify(error, null, 2)}
; 48 | } 49 | 50 | if (isExecuting) { 51 | return ( 52 |
53 | 54 |
55 | ); 56 | } 57 | 58 | if (!results) { 59 | return null; 60 | } 61 | 62 | const totalQueries = results.length; 63 | return ( 64 |
65 | {results.map((result, idx) => ( 66 | 1} 71 | key={idx} 72 | widthOffset={widthOffset} 73 | heightOffset={heightOffset} 74 | copied={copied} 75 | saved={saved} 76 | onSaveToFileClick={onSaveToFileClick} 77 | onCopyToClipboardClick={onCopyToClipboardClick} 78 | /> 79 | ))} 80 |
81 | ); 82 | }; 83 | 84 | QueryResults.displayName = 'QueryResult'; 85 | export default QueryResults; 86 | -------------------------------------------------------------------------------- /src/renderer/components/query-tab.tsx: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash'; 2 | import React, { FC, useCallback, useState } from 'react'; 3 | import { Tab } from 'react-tabs'; 4 | 5 | import * as QueryActions from '../actions/queries'; 6 | import { useAppDispatch, useAppSelector } from '../hooks/redux'; 7 | 8 | interface Props { 9 | queryId: number; 10 | tabNavPosition: number; 11 | setTabNavPosition: (position: number) => void; 12 | } 13 | 14 | const QueryTab: FC = ({ queryId, tabNavPosition, setTabNavPosition }) => { 15 | const dispatch = useAppDispatch(); 16 | const queries = useAppSelector((state) => state.queries); 17 | 18 | const [isRenaming, setIsRenaming] = useState(false); 19 | const [tabValue, setTabValue] = useState(''); 20 | 21 | const removeQuery = useCallback( 22 | (queryId: number) => { 23 | dispatch(QueryActions.removeQuery(queryId)); 24 | }, 25 | [dispatch], 26 | ); 27 | 28 | const isCurrentQuery = queryId === queries.currentQueryId; 29 | const buildContent = () => { 30 | if (isRenaming) { 31 | return ( 32 |
33 | { 38 | dispatch(QueryActions.renameQuery(tabValue)); 39 | setIsRenaming(false); 40 | }} 41 | onKeyDown={(event) => { 42 | if (event.key !== 'Escape' && event.key !== 'Enter') { 43 | return; 44 | } 45 | 46 | if (event.key === 'Enter') { 47 | dispatch(QueryActions.renameQuery(tabValue)); 48 | } 49 | 50 | setIsRenaming(false); 51 | }} 52 | defaultValue={queries.queriesById[queryId].name} 53 | /> 54 |
55 | ); 56 | } 57 | 58 | return ( 59 |
60 | {queries.queriesById[queryId].name} 61 | 70 |
71 | ); 72 | }; 73 | 74 | return ( 75 | { 78 | setIsRenaming(true); 79 | setTabValue(queries.queriesById[queryId].name); 80 | }} 81 | className={['react-tabs__tab', `item ${isCurrentQuery ? 'active' : ''}`]}> 82 | {buildContent()} 83 | 84 | ); 85 | }; 86 | 87 | QueryTab.displayName = 'QueryTab'; 88 | 89 | // Required to set `tabsRole` on the component so its understood properly by react-tabs 90 | // @ts-ignore 91 | QueryTab.tabsRole = 'Tab'; 92 | 93 | export default QueryTab; 94 | -------------------------------------------------------------------------------- /src/renderer/components/react-resizable.css: -------------------------------------------------------------------------------- 1 | .react-resizable { 2 | position: relative; 3 | box-sizing: border-box; 4 | overflow: hidden; 5 | } 6 | 7 | .react-resizable.react-resizable-se-resize { 8 | /* 9 | workaround because react-resizable requires a initial "width" in px 10 | https://github.com/STRML/react-resizable/issues/6 11 | */ 12 | width: 100% !important; 13 | max-width: 100%; /* removes scroll bar in windows version */ 14 | } 15 | 16 | .react-resizable-ew-resize { 17 | padding-right: 15px; 18 | } 19 | 20 | .react-resizable-ew-resize .react-resizable-handle { 21 | position: absolute; 22 | width: 15px; 23 | top: 7.5px; 24 | right: 7.5px; 25 | margin-top: 0; 26 | cursor: -webkit-grabbing; 27 | } 28 | 29 | .react-resizable-ew-resize .react-resizable-handle:before { 30 | content: '\f07e'; 31 | font-family: Icons; 32 | } 33 | 34 | .react-resizable-se-resize .react-resizable-handle { 35 | position: absolute; 36 | width: 20px; 37 | height: 20px; 38 | bottom: 0; 39 | right: 0; 40 | background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg08IS0tIEdlbmVyYXRvcjogQWRvYmUgRmlyZXdvcmtzIENTNiwgRXhwb3J0IFNWRyBFeHRlbnNpb24gYnkgQWFyb24gQmVhbGwgKGh0dHA6Ly9maXJld29ya3MuYWJlYWxsLmNvbSkgLiBWZXJzaW9uOiAwLjYuMSAgLS0+DTwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DTxzdmcgaWQ9IlVudGl0bGVkLVBhZ2UlMjAxIiB2aWV3Qm94PSIwIDAgNiA2IiBzdHlsZT0iYmFja2dyb3VuZC1jb2xvcjojZmZmZmZmMDAiIHZlcnNpb249IjEuMSINCXhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiDQl4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjZweCIgaGVpZ2h0PSI2cHgiDT4NCTxnIG9wYWNpdHk9IjAuMzAyIj4NCQk8cGF0aCBkPSJNIDYgNiBMIDAgNiBMIDAgNC4yIEwgNCA0LjIgTCA0LjIgNC4yIEwgNC4yIDAgTCA2IDAgTCA2IDYgTCA2IDYgWiIgZmlsbD0iIzAwMDAwMCIvPg0JPC9nPg08L3N2Zz4='); 41 | background-position: bottom right; 42 | padding: 0 3px 3px 0; 43 | background-repeat: no-repeat; 44 | background-origin: content-box; 45 | box-sizing: border-box; 46 | cursor: se-resize; 47 | } 48 | 49 | .react-resizable-handle { 50 | height: 100%; 51 | cursor: col-resize !important; 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/components/react-tabs.scss: -------------------------------------------------------------------------------- 1 | .react-tabs { 2 | width: 100%; 3 | position: relative; 4 | } 5 | 6 | .react-tabs .react-tabs__tab-list { 7 | white-space: nowrap; 8 | } 9 | 10 | .react-tabs ul.react-tabs__tab-list > li.react-tabs__tab.item { 11 | display: inline-block; 12 | } 13 | 14 | .react-tabs__tab-panel { 15 | display: none; 16 | } 17 | 18 | .react-tabs__tab-panel--selected { 19 | display: block; 20 | } 21 | 22 | #tabs-nav-wrapper { 23 | display: flex; 24 | flex-direction: row; 25 | } 26 | 27 | #tabs-nav-wrapper > .tabs-container { 28 | flex: auto; 29 | overflow: hidden; 30 | position: relative; 31 | width: 100%; 32 | padding-bottom: 3px; 33 | } 34 | 35 | #tabs-nav-wrapper > .tabs-container > ul { 36 | position: relative; 37 | margin: 0; 38 | 39 | .item > .button { 40 | opacity: 0; 41 | transition: opacity .25s ease-in-out; 42 | font-size: 0.5em; 43 | margin: -0.5em -1em 0 1.5em; 44 | } 45 | 46 | .item.active:hover > .button { 47 | opacity: 1; 48 | } 49 | } 50 | 51 | #tabs-nav-wrapper > button { 52 | font-size: 0.75em; 53 | margin: 0; 54 | border-radius: 0; 55 | border-top-left-radius: 5px; 56 | border-top-right-radius: 5px; 57 | } 58 | 59 | #tabs-nav-wrapper button.right.floated.ui.icon.button.mini { 60 | font-size: 0.5em; 61 | margin-left: 0.5em; 62 | } 63 | 64 | .react-tabs #tabs-nav-wrapper [role=tablist] { 65 | border-bottom: none; 66 | } 67 | -------------------------------------------------------------------------------- /src/renderer/components/require-context.ts: -------------------------------------------------------------------------------- 1 | export const requireClientLogo = (item: string) => 2 | require.context('../assets/server-db-client', false, /.*\.png$/)(`./${item}.png`).default; 3 | -------------------------------------------------------------------------------- /src/renderer/components/server-db-client-info-modal.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useRef } from 'react'; 2 | import { DB_CLIENTS } from '../api'; 3 | 4 | interface Props { 5 | client: string; 6 | infos: string[]; 7 | onCloseClick: () => void; 8 | } 9 | 10 | const ServerDBClientInfoModal: FC = ({ client, infos, onCloseClick }) => { 11 | const ref = useRef(null); 12 | 13 | const dbClient = DB_CLIENTS.find((item) => item.key === client); 14 | 15 | if (!dbClient) { 16 | throw new Error('Unknown client'); 17 | } 18 | 19 | useEffect(() => { 20 | if (!ref.current) { 21 | return; 22 | } 23 | const elem = ref.current; 24 | $(elem) 25 | .modal({ 26 | closable: true, 27 | detachable: false, 28 | allowMultiple: true, 29 | observeChanges: true, 30 | onHidden: () => onCloseClick(), 31 | }) 32 | .modal('show'); 33 | return () => { 34 | $(elem).modal('hide'); 35 | }; 36 | }, [ref, onCloseClick]); 37 | 38 | return ( 39 |
40 |
{dbClient.name} Query Information
41 |
42 |

43 | Some particularities about queries on 44 | {dbClient.name} you should know: 45 |

46 |
47 | {infos.map((info, idx) => ( 48 |
49 | {info} 50 |
51 | ))} 52 |
53 |
    54 |
55 |
56 | ); 57 | }; 58 | 59 | ServerDBClientInfoModal.displayName = 'ServerDBClientInfoModal'; 60 | export default ServerDBClientInfoModal; 61 | -------------------------------------------------------------------------------- /src/renderer/components/server-filter.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, ChangeEventHandler, FC, useCallback, useMemo } from 'react'; 2 | import { debounce } from 'lodash'; 3 | 4 | interface Props { 5 | onFilterChange: ChangeEventHandler; 6 | onAddClick: () => void; 7 | onSettingsClick: () => void; 8 | } 9 | 10 | const ServerFilter: FC = ({ onFilterChange, onAddClick, onSettingsClick }) => { 11 | const debouncedFilterChange = useMemo(() => debounce(onFilterChange, 200), [onFilterChange]); 12 | 13 | const handleFilterChange = useCallback( 14 | (event: ChangeEvent): void => { 15 | event.persist(); 16 | debouncedFilterChange(event); 17 | }, 18 | [debouncedFilterChange], 19 | ); 20 | 21 | return ( 22 |
25 | 26 | 27 | 30 | 33 |
34 | ); 35 | }; 36 | 37 | ServerFilter.displayName = 'ServerFilter'; 38 | 39 | export default ServerFilter; 40 | -------------------------------------------------------------------------------- /src/renderer/components/server-list-card.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { requireClientLogo } from './require-context'; 3 | import { DB_CLIENTS } from '../api'; 4 | import { Server } from '../../common/types/server'; 5 | 6 | /** 7 | * Load icons for supported database clients 8 | */ 9 | const ICONS = DB_CLIENTS.reduce((clients, dbClient) => { 10 | clients[dbClient.key] = requireClientLogo(dbClient.key); 11 | return clients; 12 | }, {}); 13 | 14 | interface Props { 15 | server: Server; 16 | onConnectClick: () => void; 17 | onEditClick: () => void; 18 | } 19 | 20 | const ServerListCard: FC = ({ server, onConnectClick, onEditClick }) => ( 21 |
22 |
23 |
26 | client 32 |
33 | 36 |
{server.name}
37 |
38 | {server.host ? `${server.host}:${server.port}` : server.socketPath} 39 | {server.ssh && ( 40 |
41 | via 42 | {server.ssh.host} 43 |
44 | )} 45 |
46 |
47 |
48 | 49 | Connect 50 |
51 |
52 | ); 53 | 54 | ServerListCard.displayName = 'ServerListCard'; 55 | 56 | export default ServerListCard; 57 | -------------------------------------------------------------------------------- /src/renderer/components/server-list-item.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { requireClientLogo } from './require-context'; 3 | import { DB_CLIENTS } from '../api'; 4 | import { Server } from '../../common/types/server'; 5 | 6 | /** 7 | * Load icons for supported database clients 8 | */ 9 | const ICONS = DB_CLIENTS.reduce((clients, dbClient) => { 10 | /* eslint no-param-reassign:0 */ 11 | clients[dbClient.key] = requireClientLogo(dbClient.key); 12 | return clients; 13 | }, {}); 14 | 15 | interface Props { 16 | server: Server; 17 | onConnectClick: () => void; 18 | onEditClick: () => void; 19 | } 20 | 21 | const ServerListItem: FC = ({ server, onConnectClick, onEditClick }) => ( 22 |
23 |
24 |
25 | client 26 |
27 |
28 |
29 | 32 |
33 |
34 | 44 |
45 |
46 |
47 |
{server.name}
48 |
49 | {server.host ? `${server.host}:${server.port}` : server.socketPath} 50 | {server.ssh &&
via {server.ssh.host}
} 51 |
52 |
53 |
54 |
55 | ); 56 | 57 | ServerListItem.displayName = 'ServerListItem'; 58 | 59 | export default ServerListItem; 60 | -------------------------------------------------------------------------------- /src/renderer/components/server-list.scss: -------------------------------------------------------------------------------- 1 | @import "breakpoint"; 2 | 3 | 4 | #server-list { 5 | /** 6 | * 1 card per row 7 | */ 8 | .ui.cards > .card { 9 | width: 100%; 10 | } 11 | 12 | /** 13 | * 2 cards per row 14 | */ 15 | @include respond-to('bigger than small') { 16 | .ui.cards { 17 | margin-left: -1em; 18 | margin-right: -1em; 19 | } 20 | 21 | .ui.cards > .card { 22 | width: calc( 50% - 2em ); 23 | margin-left: 1em; 24 | margin-right: 1em; 25 | } 26 | } 27 | 28 | /** 29 | * 4 cards per row 30 | */ 31 | @include respond-to('bigger than small-medium') { 32 | .ui.cards { 33 | margin-left: -0.75em; 34 | margin-right: -0.75em; 35 | } 36 | 37 | .ui.cards > .card { 38 | width: calc( 25% - 1.5em ); 39 | margin-left: 0.75em; 40 | margin-right: 0.75em; 41 | } 42 | } 43 | 44 | 45 | .card .content { 46 | .header, .meta { 47 | word-break: break-all; 48 | } 49 | } 50 | } 51 | 52 | body.dark-theme #server-list { 53 | .header { 54 | color: #fff; 55 | } 56 | 57 | .meta { 58 | color: #fff; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/renderer/components/server-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | 3 | import ServerListCard from './server-list-card'; 4 | import ServerListItem from './server-list-item'; 5 | import Message from './message'; 6 | import type { Server } from '../../common/types/server'; 7 | import type { ConfigState } from '../reducers/config'; 8 | 9 | require('./server-list.scss'); 10 | 11 | interface Props { 12 | servers: Server[]; 13 | onEditClick: (server: Server) => void; 14 | onConnectClick: (server: Server) => void; 15 | config: ConfigState; 16 | } 17 | 18 | const itemsPerRow = 4; 19 | 20 | const ServerList: FC = ({ servers, onEditClick, onConnectClick, config }) => { 21 | if (!servers.length) { 22 | return ; 23 | } 24 | 25 | return ( 26 |
27 | {config.data?.connectionsAsList ? ( 28 |
29 | {servers.map((server) => ( 30 | onConnectClick(server)} 33 | onEditClick={() => onEditClick(server)} 34 | server={server} 35 | /> 36 | ))} 37 |
38 | ) : ( 39 | servers 40 | .reduce((rows, item, index) => { 41 | const position = Math.floor(index / itemsPerRow); 42 | if (!rows[position]) { 43 | rows[position] = []; // eslint-disable-line no-param-reassign 44 | } 45 | 46 | rows[position].push(item); 47 | return rows; 48 | }, []) 49 | .map((row, rowIdx) => ( 50 |
51 | {row.map((server) => ( 52 | onConnectClick(server)} 55 | onEditClick={() => onEditClick(server)} 56 | server={server} 57 | /> 58 | ))} 59 |
60 | )) 61 | )} 62 |
63 | ); 64 | }; 65 | 66 | ServerList.displayName = 'ServerList'; 67 | 68 | export default ServerList; 69 | -------------------------------------------------------------------------------- /src/renderer/components/tab-list.tsx: -------------------------------------------------------------------------------- 1 | // This is a copy of the TabList from react-tabs, with addition of forwardRef so we can 2 | // calculate the width of the tab list in QueryTabs. 3 | import React, { CSSProperties, forwardRef, PropsWithChildren } from 'react'; 4 | 5 | interface Props { 6 | className?: string; 7 | style?: CSSProperties; 8 | } 9 | 10 | const TabList = forwardRef>( 11 | ({ children, className = 'react-tabs__tab-list', style }, ref) => { 12 | return ( 13 |
    14 | {children} 15 |
16 | ); 17 | }, 18 | ); 19 | 20 | TabList.displayName = 'TabList'; 21 | 22 | // Required to set `tabsRole` on the component so its understood properly by react-tabs 23 | // @ts-ignore 24 | TabList.tabsRole = 'TabList'; 25 | 26 | export default TabList; 27 | -------------------------------------------------------------------------------- /src/renderer/components/table-submenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties, useCallback, useState } from 'react'; 2 | 3 | const STYLE: Record = { 4 | header: { fontSize: '0.85em', color: '#636363' }, 5 | menu: { marginLeft: '5px' }, 6 | item: { wordBreak: 'break-all', cursor: 'default' }, 7 | }; 8 | 9 | interface Props { 10 | title: string; 11 | table: string; 12 | itemsByTable: undefined | { [table: string]: T[] }; 13 | startCollapsed?: boolean; 14 | } 15 | 16 | const TableSubmenu = ({ 17 | title, 18 | table, 19 | itemsByTable, 20 | startCollapsed = false, 21 | }: Props) => { 22 | const [collapsed, setCollapsed] = useState(startCollapsed); 23 | 24 | const toggleCollapse = useCallback(() => { 25 | setCollapsed((prev) => !prev); 26 | }, []); 27 | 28 | return ( 29 |
30 | 38 | 39 | {title} 40 | 41 |
42 | {collapsed || !itemsByTable?.[table] ? null : !itemsByTable[table].length ? ( 43 | 44 | No results found 45 | 46 | ) : ( 47 | itemsByTable[table].map((item) => ( 48 | 53 | {title === 'Columns' ? ( 54 | 55 | ) : null} 56 | {item.name} 57 | {title === 'Columns' ? ( 58 | 64 | {item.dataType} 65 | 66 | ) : null} 67 | 68 | )) 69 | )} 70 |
71 |
72 | ); 73 | }; 74 | 75 | TableSubmenu.displayName = 'TableSubmenu'; 76 | export default TableSubmenu; 77 | -------------------------------------------------------------------------------- /src/renderer/components/update-checker.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, MouseEvent, useEffect, useState } from 'react'; 2 | import { CONFIG, sqlectron } from '../api'; 3 | 4 | const repo = CONFIG.repository?.url.replace('https://github.com/', ''); 5 | const LATEST_RELEASE_URL = `https://github.com/${repo}/releases/latest`; 6 | 7 | const UpdateChecker: FC = () => { 8 | const [isVisible, setIsVisible] = useState(false); 9 | //const [currentVersion, setCurrentVersion] = useState(''); 10 | const [latestVersion, setLatestVersion] = useState(''); 11 | 12 | const updateAvailable = (/*currentVersion,*/ latestVersion) => { 13 | //setCurrentVersion(currentVersion); 14 | setLatestVersion(latestVersion); 15 | setIsVisible(true); 16 | }; 17 | 18 | const onClick = (event: MouseEvent) => { 19 | event.preventDefault(); 20 | sqlectron.browser.shell.openExternal(LATEST_RELEASE_URL); 21 | }; 22 | 23 | useEffect(() => { 24 | const unsub = sqlectron.update.onUpdateAvailable(updateAvailable); 25 | sqlectron.update.checkUpdateAvailable(); 26 | 27 | return unsub; 28 | }, []); 29 | 30 | return ( 31 | <> 32 | {isVisible && ( 33 | 34 | 35 | Update available: {latestVersion} 36 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | UpdateChecker.displayName = 'UpdateChecker'; 43 | 44 | export default UpdateChecker; 45 | -------------------------------------------------------------------------------- /src/renderer/containers/query-browser.css: -------------------------------------------------------------------------------- 1 | #sidebar { 2 | overflow-y: hidden; 3 | overflow-x: hidden; 4 | } 5 | #sidebar ::-webkit-scrollbar { 6 | display: none; 7 | } 8 | 9 | #sidebar:hover { 10 | overflow-y: auto; 11 | overflow-y: overlay; 12 | } 13 | #sidebar:hover ::-webkit-scrollbar { 14 | display: block; 15 | } 16 | 17 | #sidebar ::-webkit-scrollbar { 18 | -webkit-appearance: none; 19 | } 20 | #sidebar ::-webkit-scrollbar-thumb { 21 | box-shadow: inset 0 -2px, inset 0 -8px, inset 0 2px, inset 0 8px; 22 | min-height: 36px; 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/containers/root.tsx: -------------------------------------------------------------------------------- 1 | import { Store } from '@reduxjs/toolkit'; 2 | import React, { FC } from 'react'; 3 | import { Provider } from 'react-redux'; 4 | import { HashRouter as Router, Switch, Route } from 'react-router-dom'; 5 | 6 | import App from './app'; 7 | import ServerManagementContainer from './server-management'; 8 | import QueryBrowserContainer from './query-browser'; 9 | 10 | interface Props { 11 | store: Store; 12 | } 13 | 14 | const Root: FC = ({ store }) => ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | 27 | Root.displayName = 'Root'; 28 | export default Root; 29 | -------------------------------------------------------------------------------- /src/renderer/containers/sqlectron.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/src/renderer/containers/sqlectron.gif -------------------------------------------------------------------------------- /src/renderer/entry.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { store } from './store/configure'; 4 | import Root from './containers/root'; 5 | 6 | const doRender = (NextRoot) => { 7 | ReactDOM.render(, document.getElementById('content')); 8 | }; 9 | 10 | doRender(Root); 11 | 12 | if (module.hot) { 13 | module.hot.accept('./containers/root.tsx', () => { 14 | // eslint-disable-next-line @typescript-eslint/no-var-requires 15 | const NextRoot = require('./containers/root.tsx').default; 16 | doRender(NextRoot); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/renderer/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useEffectDebugger } from './useEffectDebugger'; 2 | export { usePrevious } from './usePrevious'; 3 | -------------------------------------------------------------------------------- /src/renderer/hooks/redux.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import type { TypedUseSelectorHook } from 'react-redux'; 3 | 4 | import type { AppDispatch, RootState } from '../store/configure'; 5 | 6 | export const useAppDispatch = () => useDispatch(); 7 | export const useAppSelector: TypedUseSelectorHook = useSelector; 8 | -------------------------------------------------------------------------------- /src/renderer/hooks/useEffectDebugger.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { usePrevious } from './usePrevious'; 3 | 4 | export const useEffectDebugger = ( 5 | effectHook: () => void, 6 | dependencies: any[], 7 | dependencyNames = [], 8 | ) => { 9 | const previousDeps = usePrevious(dependencies, []); 10 | 11 | const changedDeps = dependencies.reduce((accum, dependency, index) => { 12 | if (dependency !== previousDeps[index]) { 13 | const keyName = dependencyNames[index] || index; 14 | return { 15 | ...accum, 16 | [keyName]: { 17 | before: previousDeps[index], 18 | after: dependency, 19 | }, 20 | }; 21 | } 22 | 23 | return accum; 24 | }, {}); 25 | 26 | if (Object.keys(changedDeps).length) { 27 | // eslint-disable-next-line no-console 28 | console.log('[use-effect-debugger] ', changedDeps); 29 | } 30 | 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | useEffect(effectHook, dependencies); 33 | }; 34 | -------------------------------------------------------------------------------- /src/renderer/hooks/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const usePrevious = (value: T, initialValue?: T): T => { 4 | const ref = useRef(initialValue || null); 5 | useEffect(() => { 6 | ref.current = value; 7 | }, [value]); 8 | return ref.current as T; 9 | }; 10 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sqlectron 5 | 6 | 7 | 8 | 9 | 83 | 84 | 85 |
86 |
87 |
88 |
89 | 90 | 91 | -------------------------------------------------------------------------------- /src/renderer/reducers/columns.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as dbTypes from '../actions/databases'; 4 | import * as types from '../actions/columns'; 5 | 6 | export interface Column { 7 | name: string; 8 | dataType: string; 9 | } 10 | 11 | export interface ColumnsByTable { 12 | [table: string]: Column[]; 13 | } 14 | 15 | export interface ColumnAction extends Action { 16 | type: string; 17 | error: Error; 18 | isServerConnection: boolean; 19 | database: string; 20 | table: string; 21 | columns: Array<{ columnName: string; dataType: string }>; 22 | } 23 | 24 | export interface ColumnState { 25 | error: null | Error; 26 | didInvalidate: boolean; 27 | isFetching: { 28 | [database: string]: { 29 | [table: string]: boolean; 30 | }; 31 | }; 32 | columnsByTable: { 33 | [database: string]: ColumnsByTable; 34 | }; 35 | } 36 | 37 | const INITIAL_STATE: ColumnState = { 38 | error: null, 39 | isFetching: {}, 40 | didInvalidate: false, 41 | columnsByTable: {}, 42 | }; 43 | 44 | const columnReducer: Reducer = function ( 45 | state: ColumnState = INITIAL_STATE, 46 | action, 47 | ): ColumnState { 48 | switch (action.type) { 49 | case connTypes.CONNECTION_REQUEST: { 50 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 51 | } 52 | case types.FETCH_COLUMNS_REQUEST: { 53 | return { 54 | ...state, 55 | isFetching: { 56 | ...state.isFetching, 57 | [action.database]: { 58 | ...state.isFetching[action.database], 59 | [action.table]: true, 60 | }, 61 | }, 62 | didInvalidate: false, 63 | error: null, 64 | }; 65 | } 66 | case types.FETCH_COLUMNS_SUCCESS: { 67 | return { 68 | ...state, 69 | isFetching: { 70 | ...state.isFetching, 71 | [action.database]: { 72 | ...state.isFetching[action.database], 73 | [action.table]: false, 74 | }, 75 | }, 76 | didInvalidate: false, 77 | columnsByTable: { 78 | ...state.columnsByTable, 79 | [action.database]: { 80 | ...state.columnsByTable[action.database], 81 | [action.table]: action.columns.map((column) => ({ 82 | name: column.columnName, 83 | dataType: column.dataType, 84 | })), 85 | }, 86 | }, 87 | error: null, 88 | }; 89 | } 90 | case types.FETCH_COLUMNS_FAILURE: { 91 | return { 92 | ...state, 93 | isFetching: { 94 | ...state.isFetching, 95 | [action.database]: { 96 | ...state.isFetching[action.database], 97 | [action.table]: false, 98 | }, 99 | }, 100 | didInvalidate: true, 101 | error: action.error, 102 | }; 103 | } 104 | case dbTypes.REFRESH_DATABASES: { 105 | return { 106 | ...state, 107 | didInvalidate: true, 108 | }; 109 | } 110 | default: 111 | return state; 112 | } 113 | }; 114 | 115 | export default columnReducer; 116 | -------------------------------------------------------------------------------- /src/renderer/reducers/config.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as types from '../actions/config'; 3 | import { Config as ConfigType } from '../../common/types/config'; 4 | 5 | export interface Config { 6 | database: string; 7 | queryHistory: Array; 8 | } 9 | 10 | export interface ConfigAction extends Action { 11 | error: Error; 12 | type: string; 13 | config: ConfigType; 14 | path: string; 15 | } 16 | 17 | export interface ConfigState { 18 | isSaving: boolean; 19 | isEditing: boolean; 20 | path: null | string; 21 | data: null | ConfigType; 22 | error: null | Error; 23 | isLoaded: boolean; 24 | } 25 | 26 | const INITIAL_STATE: ConfigState = { 27 | isSaving: false, 28 | isEditing: false, 29 | path: null, 30 | data: null, 31 | error: null, 32 | isLoaded: false, 33 | }; 34 | 35 | const configReducer: Reducer = function ( 36 | state: ConfigState = INITIAL_STATE, 37 | action, 38 | ): ConfigState { 39 | switch (action.type) { 40 | case types.LOAD_CONFIG_SUCCESS: 41 | return { 42 | ...state, 43 | data: action.config, 44 | path: action.path, 45 | isLoaded: true, 46 | }; 47 | case types.LOAD_CONFIG_FAILURE: { 48 | return { 49 | ...state, 50 | error: action.error, 51 | isLoaded: true, 52 | }; 53 | } 54 | case types.START_EDITING_CONFIG: { 55 | return { 56 | ...state, 57 | isSaving: false, 58 | isEditing: true, 59 | }; 60 | } 61 | case types.FINISH_EDITING_CONFIG: { 62 | return { 63 | ...state, 64 | isSaving: false, 65 | isEditing: false, 66 | isLoaded: true, 67 | error: null, 68 | }; 69 | } 70 | case types.SAVE_CONFIG_REQUEST: { 71 | return { 72 | ...state, 73 | isSaving: true, 74 | }; 75 | } 76 | case types.SAVE_CONFIG_SUCCESS: { 77 | return { 78 | ...state, 79 | data: { 80 | ...state.data, 81 | ...action.config, 82 | }, 83 | isSaving: false, 84 | }; 85 | } 86 | default: 87 | return state; 88 | } 89 | }; 90 | 91 | export default configReducer; 92 | -------------------------------------------------------------------------------- /src/renderer/reducers/connections.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as types from '../actions/connections'; 3 | import * as serverTypes from '../actions/servers'; 4 | import { DB_CLIENTS } from '../api'; 5 | import { Server } from '../../common/types/server'; 6 | import { Config as ConfigType } from '../../common/types/config'; 7 | 8 | export interface ConnectionAction extends Action { 9 | type: string; 10 | error: Error; 11 | server: Server; 12 | database: string; 13 | reconnecting: boolean; 14 | config?: ConfigType; 15 | } 16 | 17 | export interface ConnectionState { 18 | error: null | Error; 19 | connected: boolean; 20 | connecting: boolean; 21 | server: null | Server; 22 | disabledFeatures: null | Array; 23 | waitingPrivateKeyPassphrase: boolean; 24 | databases: Array; 25 | 26 | // testing connection props 27 | testConnected: boolean; 28 | testConnecting: boolean; 29 | testServer: null | Server; 30 | testError: Error | null; 31 | } 32 | 33 | const INITIAL_STATE: ConnectionState = { 34 | error: null, 35 | connected: false, 36 | connecting: false, 37 | server: null, 38 | disabledFeatures: null, 39 | waitingPrivateKeyPassphrase: false, 40 | databases: [], // connected databases 41 | 42 | // testing connection props 43 | testConnected: false, 44 | testConnecting: false, 45 | testServer: null, 46 | testError: null, 47 | }; 48 | 49 | const connectionReducer: Reducer = function ( 50 | state: ConnectionState = INITIAL_STATE, 51 | action, 52 | ): ConnectionState { 53 | switch (action.type) { 54 | case types.CONNECTION_SET_CONNECTING: { 55 | return { 56 | ...INITIAL_STATE, 57 | connecting: true, 58 | }; 59 | } 60 | case types.CONNECTION_REQUEST: { 61 | const dbClient = DB_CLIENTS.find((dbClient) => dbClient.key === action.server.client); 62 | return { 63 | ...state, 64 | server: action.server, 65 | disabledFeatures: dbClient?.disabledFeatures || [], 66 | connected: false, 67 | connecting: true, 68 | }; 69 | } 70 | case types.CONNECTION_REQUIRE_SSH_PASSPHRASE: { 71 | return { ...state, waitingPrivateKeyPassphrase: true }; 72 | } 73 | case types.CONNECTION_SUCCESS: { 74 | return { 75 | ...state, 76 | connected: true, 77 | connecting: false, 78 | waitingPrivateKeyPassphrase: false, 79 | databases: [...state.databases, action.database], 80 | }; 81 | } 82 | case types.CONNECTION_FAILURE: { 83 | return { 84 | ...state, 85 | connected: false, 86 | connecting: false, 87 | waitingPrivateKeyPassphrase: false, 88 | error: action.error, 89 | }; 90 | } 91 | case types.TEST_CONNECTION_REQUEST: { 92 | const { server } = action; 93 | return { 94 | ...INITIAL_STATE, 95 | testConnected: false, 96 | testConnecting: true, 97 | testServer: server, 98 | }; 99 | } 100 | case types.TEST_CONNECTION_SUCCESS: { 101 | if (!isSameTestConnection(state, action)) return state; 102 | return { ...state, testConnected: true, testConnecting: false }; 103 | } 104 | case types.TEST_CONNECTION_FAILURE: { 105 | if (!isSameTestConnection(state, action)) return state; 106 | return { 107 | ...state, 108 | testConnected: false, 109 | testConnecting: false, 110 | testError: action.error, 111 | }; 112 | } 113 | case types.CLOSE_CONNECTION: 114 | case serverTypes.START_EDITING_SERVER: 115 | case serverTypes.FINISH_EDITING_SERVER: { 116 | return INITIAL_STATE; 117 | } 118 | 119 | default: 120 | return state; 121 | } 122 | }; 123 | 124 | function isSameTestConnection(state, action) { 125 | return state.testServer === action.server; 126 | } 127 | 128 | export default connectionReducer; 129 | -------------------------------------------------------------------------------- /src/renderer/reducers/databases.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as queryTypes from '../actions/queries'; 4 | import * as types from '../actions/databases'; 5 | 6 | export interface Database { 7 | name: string; 8 | } 9 | 10 | export interface DatabaseAction extends Action { 11 | error: Error; 12 | type: string; 13 | isServerConnection: boolean; 14 | databases: Array; 15 | name: string; 16 | fileName: string; 17 | diagramJSON: unknown; 18 | results: Array<{ command: string }>; 19 | } 20 | 21 | export interface DatabaseState { 22 | error: Error | null; 23 | isFetching: boolean; 24 | didInvalidate: boolean; 25 | items: Array; 26 | showingDiagram: boolean; 27 | diagramDatabase: string | null; 28 | fileName: null; 29 | diagramJSON: null; 30 | isSaving: boolean; 31 | } 32 | 33 | const INITIAL_STATE: DatabaseState = { 34 | error: null, 35 | isFetching: false, 36 | didInvalidate: false, 37 | items: [], 38 | showingDiagram: false, 39 | diagramDatabase: null, 40 | fileName: null, 41 | diagramJSON: null, 42 | isSaving: false, 43 | }; 44 | 45 | const COMMANDS_TRIGER_REFRESH = ['CREATE_DATABASE', 'DROP_DATABASE']; 46 | 47 | const databaseReducer: Reducer = function ( 48 | state: DatabaseState = INITIAL_STATE, 49 | action, 50 | ): DatabaseState { 51 | switch (action.type) { 52 | case connTypes.CONNECTION_REQUEST: { 53 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 54 | } 55 | case types.FETCH_DATABASES_REQUEST: { 56 | return { 57 | ...state, 58 | isFetching: true, 59 | didInvalidate: false, 60 | error: null, 61 | }; 62 | } 63 | case types.FETCH_DATABASES_SUCCESS: { 64 | return { 65 | ...state, 66 | isFetching: false, 67 | didInvalidate: false, 68 | items: action.databases.map((name) => ({ name })), 69 | error: null, 70 | }; 71 | } 72 | case types.FETCH_DATABASES_FAILURE: { 73 | return { 74 | ...state, 75 | isFetching: false, 76 | didInvalidate: true, 77 | error: action.error, 78 | }; 79 | } 80 | case types.SHOW_DATABASE_DIAGRAM: { 81 | return { 82 | ...state, 83 | showingDiagram: true, 84 | diagramDatabase: action.name, 85 | }; 86 | } 87 | case types.CLOSE_DATABASE_DIAGRAM: { 88 | return { 89 | ...state, 90 | showingDiagram: false, 91 | diagramDatabase: null, 92 | diagramJSON: null, 93 | }; 94 | } 95 | case types.GENERATE_DATABASE_DIAGRAM: { 96 | return { 97 | ...state, 98 | fileName: null, 99 | }; 100 | } 101 | case types.SAVE_DIAGRAM_REQUEST: 102 | case types.EXPORT_DIAGRAM_REQUEST: { 103 | return { 104 | ...state, 105 | isSaving: true, 106 | }; 107 | } 108 | case types.SAVE_DIAGRAM_SUCCESS: 109 | case types.EXPORT_DIAGRAM_SUCCESS: { 110 | return { 111 | ...state, 112 | fileName: action.fileName, 113 | isSaving: false, 114 | }; 115 | } 116 | case types.OPEN_DIAGRAM_SUCCESS: { 117 | return { 118 | ...state, 119 | fileName: action.fileName, 120 | diagramJSON: action.diagramJSON, 121 | }; 122 | } 123 | case types.SAVE_DIAGRAM_FAILURE: 124 | case types.EXPORT_DIAGRAM_FAILURE: 125 | case types.OPEN_DIAGRAM_FAILURE: { 126 | return { 127 | ...state, 128 | error: action.error, 129 | isSaving: false, 130 | }; 131 | } 132 | case queryTypes.EXECUTE_QUERY_SUCCESS: { 133 | return { 134 | ...state, 135 | didInvalidate: action.results.some(({ command }) => 136 | COMMANDS_TRIGER_REFRESH.includes(command), 137 | ), 138 | }; 139 | } 140 | default: 141 | return state; 142 | } 143 | }; 144 | 145 | export default databaseReducer; 146 | -------------------------------------------------------------------------------- /src/renderer/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, Reducer, AnyAction } from 'redux'; 2 | import { ThunkAction } from 'redux-thunk'; 3 | import config, { ConfigState } from './config'; 4 | import databases, { DatabaseState } from './databases'; 5 | import servers, { ServerState } from './servers'; 6 | import queries, { QueryState } from './queries'; 7 | import connections, { ConnectionState } from './connections'; 8 | import schemas, { SchemaState } from './schemas'; 9 | import tables, { TableState } from './tables'; 10 | import status from './status'; 11 | import views, { ViewState } from './views'; 12 | import routines, { RoutineState } from './routines'; 13 | import columns, { ColumnState } from './columns'; 14 | import triggers, { TriggerState } from './triggers'; 15 | import indexes, { IndexState } from './indexes'; 16 | import sqlscripts, { ScriptState } from './sqlscripts'; 17 | import keys, { KeyState } from './keys'; 18 | 19 | export type ThunkResult = ThunkAction; 20 | 21 | // The top-level state object 22 | export interface ApplicationState { 23 | config: ConfigState; 24 | databases: DatabaseState; 25 | servers: ServerState; 26 | queries: QueryState; 27 | connections: ConnectionState; 28 | schemas: SchemaState; 29 | tables: TableState; 30 | status: string; 31 | views: ViewState; 32 | routines: RoutineState; 33 | columns: ColumnState; 34 | triggers: TriggerState; 35 | indexes: IndexState; 36 | sqlscripts: ScriptState; 37 | keys: KeyState; 38 | } 39 | 40 | const rootReducer: Reducer = combineReducers({ 41 | config, 42 | databases, 43 | servers, 44 | queries, 45 | connections, 46 | schemas, 47 | tables, 48 | status, 49 | views, 50 | routines, 51 | columns, 52 | triggers, 53 | indexes, 54 | sqlscripts, 55 | keys, 56 | }); 57 | 58 | export default rootReducer; 59 | -------------------------------------------------------------------------------- /src/renderer/reducers/indexes.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as dbTypes from '../actions/databases'; 4 | import * as types from '../actions/indexes'; 5 | 6 | export interface Index { 7 | name: string; 8 | } 9 | 10 | export interface IndexesByTable { 11 | [table: string]: Index[]; 12 | } 13 | 14 | export interface IndexAction extends Action { 15 | type: string; 16 | error: Error; 17 | isServerConnection: boolean; 18 | database: string; 19 | table: string; 20 | indexes: Array; 21 | } 22 | 23 | export interface IndexState { 24 | isFetching: boolean; 25 | didInvalidate: boolean; 26 | indexesByTable: { 27 | [database: string]: IndexesByTable; 28 | }; 29 | error: null | Error; 30 | } 31 | 32 | const INITIAL_STATE: IndexState = { 33 | isFetching: false, 34 | didInvalidate: false, 35 | indexesByTable: {}, 36 | error: null, 37 | }; 38 | 39 | const indexReducer: Reducer = function ( 40 | state: IndexState = INITIAL_STATE, 41 | action, 42 | ): IndexState { 43 | switch (action.type) { 44 | case connTypes.CONNECTION_REQUEST: { 45 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 46 | } 47 | case types.FETCH_INDEXES_REQUEST: { 48 | return { 49 | ...state, 50 | isFetching: true, 51 | didInvalidate: false, 52 | error: null, 53 | }; 54 | } 55 | case types.FETCH_INDEXES_SUCCESS: { 56 | return { 57 | ...state, 58 | isFetching: false, 59 | didInvalidate: false, 60 | indexesByTable: { 61 | ...state.indexesByTable, 62 | [action.database]: { 63 | ...state.indexesByTable[action.database], 64 | [action.table]: action.indexes.map((name) => ({ name })), 65 | }, 66 | }, 67 | error: null, 68 | }; 69 | } 70 | case types.FETCH_INDEXES_FAILURE: { 71 | return { 72 | ...state, 73 | isFetching: false, 74 | didInvalidate: true, 75 | error: action.error, 76 | }; 77 | } 78 | case dbTypes.REFRESH_DATABASES: { 79 | return { 80 | ...state, 81 | didInvalidate: true, 82 | }; 83 | } 84 | default: 85 | return state; 86 | } 87 | }; 88 | 89 | export default indexReducer; 90 | -------------------------------------------------------------------------------- /src/renderer/reducers/keys.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as dbTypes from '../actions/databases'; 4 | import * as types from '../actions/keys'; 5 | 6 | export interface Key { 7 | name: string; 8 | } 9 | 10 | export interface KeyAction extends Action { 11 | type: string; 12 | error: Error; 13 | isServerConnection: boolean; 14 | database: string; 15 | table: string; 16 | tableKeys: Array; 17 | } 18 | 19 | export interface KeyState { 20 | error: null | Error; 21 | didInvalidate: boolean; 22 | isFetching: { 23 | [database: string]: { 24 | [table: string]: boolean; 25 | }; 26 | }; 27 | keysByTable: { 28 | [table: string]: Key; 29 | }; 30 | } 31 | 32 | const INITIAL_STATE: KeyState = { 33 | error: null, 34 | isFetching: {}, 35 | didInvalidate: false, 36 | keysByTable: {}, 37 | }; 38 | 39 | const keyReducer: Reducer = function (state: KeyState = INITIAL_STATE, action): KeyState { 40 | switch (action.type) { 41 | case connTypes.CONNECTION_REQUEST: { 42 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 43 | } 44 | 45 | case types.FETCH_KEYS_REQUEST: { 46 | return { 47 | ...state, 48 | isFetching: { 49 | ...state.isFetching, 50 | [action.database]: { 51 | ...state.isFetching[action.database], 52 | [action.table]: true, 53 | }, 54 | }, 55 | didInvalidate: false, 56 | error: null, 57 | }; 58 | } 59 | case types.FETCH_KEYS_SUCCESS: { 60 | return { 61 | ...state, 62 | isFetching: { 63 | ...state.isFetching, 64 | [action.database]: { 65 | ...state.isFetching[action.database], 66 | [action.table]: false, 67 | }, 68 | }, 69 | didInvalidate: false, 70 | keysByTable: { 71 | ...state.keysByTable, 72 | [action.database]: { 73 | ...state.keysByTable[action.database], 74 | [action.table]: action.tableKeys, 75 | }, 76 | }, 77 | error: null, 78 | }; 79 | } 80 | case types.FETCH_KEYS_FAILURE: { 81 | return { 82 | ...state, 83 | isFetching: { 84 | ...state.isFetching, 85 | [action.database]: { 86 | ...state.isFetching[action.database], 87 | [action.table]: false, 88 | }, 89 | }, 90 | didInvalidate: true, 91 | error: action.error, 92 | }; 93 | } 94 | case dbTypes.REFRESH_DATABASES: { 95 | return { 96 | ...state, 97 | didInvalidate: true, 98 | }; 99 | } 100 | default: 101 | return state; 102 | } 103 | }; 104 | 105 | export default keyReducer; 106 | -------------------------------------------------------------------------------- /src/renderer/reducers/routines.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as dbTypes from '../actions/databases'; 4 | import * as types from '../actions/routines'; 5 | 6 | export interface Routine { 7 | schema: string; 8 | name: string; 9 | routineDefinition: string; 10 | } 11 | 12 | export interface RoutineAction extends Action { 13 | type: string; 14 | error: Error; 15 | isServerConnection: boolean; 16 | database: string; 17 | routines: Array<{ 18 | schema: string; 19 | routineName: string; 20 | routineDefinition: string; 21 | routineType: string; 22 | }>; 23 | } 24 | 25 | export interface RoutineState { 26 | error: null | Error; 27 | isFetching: boolean; 28 | didInvalidate: boolean; 29 | functionsByDatabase: { 30 | [db: string]: Array; 31 | }; 32 | proceduresByDatabase: { 33 | [db: string]: Array; 34 | }; 35 | } 36 | 37 | const INITIAL_STATE: RoutineState = { 38 | error: null, 39 | isFetching: false, 40 | didInvalidate: false, 41 | functionsByDatabase: {}, 42 | proceduresByDatabase: {}, 43 | }; 44 | 45 | const routineReducer: Reducer = function ( 46 | state: RoutineState = INITIAL_STATE, 47 | action, 48 | ): RoutineState { 49 | switch (action.type) { 50 | case connTypes.CONNECTION_REQUEST: { 51 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 52 | } 53 | case types.FETCH_ROUTINES_REQUEST: { 54 | return { 55 | ...state, 56 | isFetching: true, 57 | didInvalidate: false, 58 | error: null, 59 | }; 60 | } 61 | case types.FETCH_ROUTINES_SUCCESS: { 62 | return { 63 | ...state, 64 | isFetching: false, 65 | didInvalidate: false, 66 | functionsByDatabase: { 67 | ...state.functionsByDatabase, 68 | [action.database]: action.routines.filter(isFunction).map((routine) => ({ 69 | schema: routine.schema, 70 | name: routine.routineName, 71 | routineDefinition: routine.routineDefinition, 72 | })), 73 | }, 74 | proceduresByDatabase: { 75 | ...state.proceduresByDatabase, 76 | [action.database]: action.routines.filter(isProcedure).map((routine) => ({ 77 | schema: routine.schema, 78 | name: routine.routineName, 79 | routineDefinition: routine.routineDefinition, 80 | })), 81 | }, 82 | error: null, 83 | }; 84 | } 85 | case types.FETCH_ROUTINES_FAILURE: { 86 | return { 87 | ...state, 88 | isFetching: false, 89 | didInvalidate: true, 90 | error: action.error, 91 | }; 92 | } 93 | case dbTypes.REFRESH_DATABASES: { 94 | return { 95 | ...state, 96 | didInvalidate: true, 97 | }; 98 | } 99 | default: 100 | return state; 101 | } 102 | }; 103 | 104 | function isFunction(routine) { 105 | return routine.routineType === 'FUNCTION'; 106 | } 107 | 108 | function isProcedure(routine) { 109 | return routine.routineType === 'PROCEDURE'; 110 | } 111 | export default routineReducer; 112 | -------------------------------------------------------------------------------- /src/renderer/reducers/schemas.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as dbTypes from '../actions/databases'; 4 | import * as queryTypes from '../actions/queries'; 5 | import * as types from '../actions/schemas'; 6 | 7 | export interface Schema { 8 | name: string; 9 | } 10 | 11 | export interface SchemaAction extends Action { 12 | type: string; 13 | error: Error; 14 | schemas: Array; 15 | isServerConnection: boolean; 16 | database: string; 17 | results: Array<{ command: string }>; 18 | } 19 | 20 | export interface SchemaState { 21 | error: null | Error; 22 | isFetching: boolean; 23 | didInvalidate: boolean; 24 | itemsByDatabase: { 25 | [db: string]: Array; 26 | }; 27 | selectedSchemasForDiagram: []; 28 | } 29 | 30 | const INITIAL_STATE: SchemaState = { 31 | error: null, 32 | isFetching: false, 33 | didInvalidate: false, 34 | itemsByDatabase: {}, 35 | selectedSchemasForDiagram: [], 36 | }; 37 | 38 | const COMMANDS_TRIGER_REFRESH = ['CREATE_SCHEMA', 'DROP_SCHEMA']; 39 | 40 | const schemaReducer: Reducer = function ( 41 | state: SchemaState = INITIAL_STATE, 42 | action, 43 | ): SchemaState { 44 | switch (action.type) { 45 | case connTypes.CONNECTION_REQUEST: { 46 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 47 | } 48 | case types.FETCH_SCHEMAS_REQUEST: { 49 | return { 50 | ...state, 51 | isFetching: true, 52 | didInvalidate: false, 53 | error: null, 54 | }; 55 | } 56 | case types.FETCH_SCHEMAS_SUCCESS: { 57 | return { 58 | ...state, 59 | isFetching: false, 60 | didInvalidate: false, 61 | itemsByDatabase: { 62 | ...state.itemsByDatabase, 63 | [action.database]: action.schemas.map((name) => ({ name })), 64 | }, 65 | error: null, 66 | }; 67 | } 68 | case types.FETCH_SCHEMAS_FAILURE: { 69 | return { 70 | ...state, 71 | isFetching: false, 72 | didInvalidate: true, 73 | error: action.error, 74 | }; 75 | } 76 | case queryTypes.EXECUTE_QUERY_SUCCESS: { 77 | return { 78 | ...state, 79 | didInvalidate: action.results.some(({ command }) => 80 | COMMANDS_TRIGER_REFRESH.includes(command), 81 | ), 82 | }; 83 | } 84 | case dbTypes.REFRESH_DATABASES: { 85 | return { 86 | ...state, 87 | didInvalidate: true, 88 | }; 89 | } 90 | case dbTypes.CLOSE_DATABASE_DIAGRAM: { 91 | return { 92 | ...state, 93 | selectedSchemasForDiagram: [], 94 | }; 95 | } 96 | default: 97 | return state; 98 | } 99 | }; 100 | export default schemaReducer; 101 | -------------------------------------------------------------------------------- /src/renderer/reducers/servers.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as types from '../actions/servers'; 3 | import * as configTypes from '../actions/config'; 4 | import { Server } from '../../common/types/server'; 5 | 6 | export interface ValidationError { 7 | validationErrors: Array; 8 | } 9 | 10 | export interface ServerAction extends Action { 11 | type: string; 12 | error: ValidationError; 13 | server: Server; 14 | config: { 15 | servers: Array; 16 | }; 17 | id: string; 18 | } 19 | 20 | export interface ServerState { 21 | error: null | ValidationError; 22 | isSaving: boolean; 23 | isEditing: boolean; 24 | items: Array; 25 | editingServer: null | Server; 26 | } 27 | 28 | const INITIAL_STATE: ServerState = { 29 | error: null, 30 | isSaving: false, 31 | isEditing: false, 32 | items: [], 33 | editingServer: null, 34 | }; 35 | 36 | const serverReducer: Reducer = function ( 37 | state: ServerState = INITIAL_STATE, 38 | action, 39 | ): ServerState { 40 | switch (action.type) { 41 | case configTypes.LOAD_CONFIG_SUCCESS: 42 | return { 43 | ...state, 44 | items: action.config.servers, 45 | }; 46 | case types.START_EDITING_SERVER: { 47 | return { 48 | ...state, 49 | isSaving: false, 50 | isEditing: true, 51 | editingServer: action.server || null, 52 | }; 53 | } 54 | case types.FINISH_EDITING_SERVER: { 55 | return { 56 | ...state, 57 | isSaving: false, 58 | isEditing: false, 59 | editingServer: null, 60 | error: null, 61 | }; 62 | } 63 | case types.SAVE_SERVER_REQUEST: 64 | case types.DUPLICATE_SERVER_REQUEST: 65 | case types.REMOVE_SERVER_REQUEST: { 66 | return { 67 | ...state, 68 | isSaving: true, 69 | }; 70 | } 71 | case types.SAVE_SERVER_SUCCESS: 72 | case types.DUPLICATE_SERVER_SUCCESS: { 73 | return { 74 | ...state, 75 | items: save(state.items, action.server), 76 | error: null, 77 | isSaving: false, 78 | isEditing: false, 79 | }; 80 | } 81 | case types.REMOVE_SERVER_SUCCESS: 82 | return { 83 | ...state, 84 | items: remove(state.items, action.id), 85 | error: null, 86 | isSaving: false, 87 | isEditing: false, 88 | }; 89 | case types.SAVE_SERVER_FAILURE: 90 | case types.DUPLICATE_SERVER_FAILURE: 91 | case types.REMOVE_SERVER_FAILURE: { 92 | return { 93 | ...state, 94 | error: action.error.validationErrors, 95 | isSaving: false, 96 | }; 97 | } 98 | default: 99 | return state; 100 | } 101 | }; 102 | 103 | function save(dataItems, server) { 104 | const items = [...dataItems] || []; 105 | const index = server.id && items.findIndex((srv) => srv.id === server.id); 106 | if (index >= 0) { 107 | items[index] = server; 108 | } else { 109 | items.push(server); 110 | } 111 | return items; 112 | } 113 | 114 | function remove(items, id) { 115 | const index = items.findIndex((srv) => srv.id === id); 116 | return [...items.slice(0, index), ...items.slice(index + 1)]; 117 | } 118 | 119 | export default serverReducer; 120 | -------------------------------------------------------------------------------- /src/renderer/reducers/sqlscripts.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as types from '../actions/sqlscripts'; 4 | 5 | export type ActionType = 'CREATE' | 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE'; 6 | export type ObjectType = 'Table' | 'View' | 'Function' | 'Procedure'; 7 | 8 | export interface ScriptAction extends Action { 9 | type: string; 10 | error: Error; 11 | isServerConnection: boolean; 12 | database: string; 13 | item: string; 14 | script: string; 15 | objectType: ObjectType; 16 | actionType: ActionType; 17 | } 18 | 19 | export interface ScriptState { 20 | error: null | Error; 21 | isFetching: boolean; 22 | didInvalidate: boolean; 23 | scriptsByObject: { 24 | [db: string]: { 25 | [item: string]: { 26 | CREATE?: string; 27 | SELECT?: string; 28 | INSERT?: string; 29 | UPDATE?: string; 30 | DELETE?: string; 31 | objectType: ObjectType; 32 | }; 33 | }; 34 | }; 35 | } 36 | 37 | const INITIAL_STATE: ScriptState = { 38 | error: null, 39 | isFetching: false, 40 | didInvalidate: false, 41 | scriptsByObject: {}, 42 | }; 43 | 44 | const scriptReducer: Reducer = function ( 45 | state: ScriptState = INITIAL_STATE, 46 | action, 47 | ): ScriptState { 48 | switch (action.type) { 49 | case connTypes.CONNECTION_REQUEST: { 50 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 51 | } 52 | case types.GET_SCRIPT_REQUEST: { 53 | return { 54 | ...state, 55 | isFetching: true, 56 | didInvalidate: false, 57 | error: null, 58 | }; 59 | } 60 | case types.GET_SCRIPT_SUCCESS: { 61 | const scriptsByItem = !state.scriptsByObject[action.database] 62 | ? {} 63 | : state.scriptsByObject[action.database][action.item]; 64 | return { 65 | ...state, 66 | isFetching: false, 67 | didInvalidate: false, 68 | error: null, 69 | scriptsByObject: { 70 | ...state.scriptsByObject, 71 | [action.database]: { 72 | ...state.scriptsByObject[action.database], 73 | [action.item]: { 74 | ...scriptsByItem, 75 | objectType: action.objectType, 76 | [action.actionType]: action.script, 77 | }, 78 | }, 79 | }, 80 | }; 81 | } 82 | case types.GET_SCRIPT_FAILURE: { 83 | return { 84 | ...state, 85 | isFetching: false, 86 | didInvalidate: true, 87 | error: action.error, 88 | }; 89 | } 90 | default: 91 | return state; 92 | } 93 | }; 94 | 95 | export default scriptReducer; 96 | -------------------------------------------------------------------------------- /src/renderer/reducers/status.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as tablesTypes from '../actions/tables'; 4 | import * as queriesTypes from '../actions/queries'; 5 | 6 | export interface StatusAction extends Action { 7 | type: string; 8 | error: null | Error; 9 | } 10 | 11 | const INITIAL_STATE = ''; 12 | 13 | const statusReducer: Reducer = function (_, action) { 14 | switch (action.type) { 15 | case connTypes.CONNECTION_REQUEST: 16 | return 'Connecting to database...'; 17 | case connTypes.CONNECTION_SUCCESS: 18 | return 'Connection to database established'; 19 | case tablesTypes.FETCH_TABLES_REQUEST: 20 | return 'Loading list of tables...'; 21 | case queriesTypes.EXECUTE_QUERY_REQUEST: 22 | return 'Executing query...'; 23 | case queriesTypes.SAVE_QUERY_REQUEST: 24 | return 'Saving query...'; 25 | case queriesTypes.SAVE_QUERY_SUCCESS: 26 | return 'Query saved successfully'; 27 | case queriesTypes.SAVE_QUERY_FAILURE: 28 | return `Error saving query. ${action.error?.message}`; 29 | default: 30 | return INITIAL_STATE; 31 | } 32 | }; 33 | 34 | export default statusReducer; 35 | -------------------------------------------------------------------------------- /src/renderer/reducers/tables.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import { DbTable } from '../../common/types/database'; 3 | import * as connTypes from '../actions/connections'; 4 | import * as dbTypes from '../actions/databases'; 5 | import * as queryTypes from '../actions/queries'; 6 | import * as types from '../actions/tables'; 7 | 8 | export interface TableAction extends Action { 9 | type: string; 10 | error: Error; 11 | isServerConnection: boolean; 12 | tables: Array; 13 | database: string; 14 | results: Array<{ command: string }>; 15 | } 16 | 17 | export interface TableState { 18 | error: null | Error; 19 | isFetching: boolean; 20 | didInvalidate: boolean; 21 | itemsByDatabase: { 22 | [database: string]: DbTable[]; 23 | }; 24 | selectedTablesForDiagram: Array; 25 | } 26 | 27 | const INITIAL_STATE: TableState = { 28 | error: null, 29 | isFetching: false, 30 | didInvalidate: false, 31 | itemsByDatabase: {}, 32 | selectedTablesForDiagram: [], 33 | }; 34 | 35 | const COMMANDS_TRIGER_REFRESH = ['CREATE_TABLE', 'DROP_TABLE']; 36 | 37 | const tableReducer: Reducer = function ( 38 | state: TableState = INITIAL_STATE, 39 | action, 40 | ): TableState { 41 | switch (action.type) { 42 | case connTypes.CONNECTION_REQUEST: { 43 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 44 | } 45 | case types.SELECT_TABLES_FOR_DIAGRAM: { 46 | return { 47 | ...state, 48 | selectedTablesForDiagram: action.tables, 49 | }; 50 | } 51 | case types.FETCH_TABLES_REQUEST: { 52 | return { 53 | ...state, 54 | isFetching: true, 55 | didInvalidate: false, 56 | error: null, 57 | }; 58 | } 59 | case types.FETCH_TABLES_SUCCESS: { 60 | return { 61 | ...state, 62 | isFetching: false, 63 | didInvalidate: false, 64 | itemsByDatabase: { 65 | ...state.itemsByDatabase, 66 | [action.database]: action.tables, 67 | }, 68 | error: null, 69 | }; 70 | } 71 | case types.FETCH_TABLES_FAILURE: { 72 | return { 73 | ...state, 74 | isFetching: false, 75 | didInvalidate: true, 76 | error: action.error, 77 | }; 78 | } 79 | case queryTypes.EXECUTE_QUERY_SUCCESS: { 80 | return { 81 | ...state, 82 | didInvalidate: action.results.some(({ command }) => 83 | COMMANDS_TRIGER_REFRESH.includes(command), 84 | ), 85 | }; 86 | } 87 | case dbTypes.REFRESH_DATABASES: { 88 | return { 89 | ...state, 90 | didInvalidate: true, 91 | }; 92 | } 93 | case dbTypes.CLOSE_DATABASE_DIAGRAM: { 94 | return { 95 | ...state, 96 | selectedTablesForDiagram: [], 97 | }; 98 | } 99 | default: 100 | return state; 101 | } 102 | }; 103 | 104 | export default tableReducer; 105 | -------------------------------------------------------------------------------- /src/renderer/reducers/triggers.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as dbTypes from '../actions/databases'; 4 | import * as types from '../actions/triggers'; 5 | 6 | export interface Trigger { 7 | name: string; 8 | } 9 | 10 | export interface TriggersByTable { 11 | [table: string]: Trigger[]; 12 | } 13 | 14 | export interface TriggerAction extends Action { 15 | type: string; 16 | error: Error; 17 | isServerConnection: boolean; 18 | database: string; 19 | table: string; 20 | triggers: Array; 21 | } 22 | 23 | export interface TriggerState { 24 | error: null | Error; 25 | isFetching: boolean; 26 | didInvalidate: boolean; 27 | triggersByTable: { 28 | [database: string]: TriggersByTable; 29 | }; 30 | } 31 | 32 | const INITIAL_STATE: TriggerState = { 33 | error: null, 34 | isFetching: false, 35 | didInvalidate: false, 36 | triggersByTable: {}, 37 | }; 38 | 39 | const triggerReducer: Reducer = function ( 40 | state: TriggerState = INITIAL_STATE, 41 | action, 42 | ): TriggerState { 43 | switch (action.type) { 44 | case connTypes.CONNECTION_REQUEST: { 45 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 46 | } 47 | case types.FETCH_TRIGGERS_REQUEST: { 48 | return { 49 | ...state, 50 | isFetching: true, 51 | didInvalidate: false, 52 | error: null, 53 | }; 54 | } 55 | case types.FETCH_TRIGGERS_SUCCESS: { 56 | return { 57 | ...state, 58 | isFetching: false, 59 | didInvalidate: false, 60 | triggersByTable: { 61 | ...state.triggersByTable, 62 | [action.database]: { 63 | ...state.triggersByTable[action.database], 64 | [action.table]: action.triggers.map((name) => ({ name })), 65 | }, 66 | }, 67 | error: null, 68 | }; 69 | } 70 | case types.FETCH_TRIGGERS_FAILURE: { 71 | return { 72 | ...state, 73 | isFetching: false, 74 | didInvalidate: true, 75 | error: action.error, 76 | }; 77 | } 78 | case dbTypes.REFRESH_DATABASES: { 79 | return { 80 | ...state, 81 | didInvalidate: true, 82 | }; 83 | } 84 | default: 85 | return state; 86 | } 87 | }; 88 | 89 | export default triggerReducer; 90 | -------------------------------------------------------------------------------- /src/renderer/reducers/views.ts: -------------------------------------------------------------------------------- 1 | import { Action, Reducer } from 'redux'; 2 | import * as connTypes from '../actions/connections'; 3 | import * as types from '../actions/views'; 4 | import * as dbTypes from '../actions/databases'; 5 | import { DbTable } from '../../common/types/database'; 6 | 7 | export interface ViewAction extends Action { 8 | type: string; 9 | error: Error; 10 | isServerConnection: boolean; 11 | views: Array; 12 | database: string; 13 | } 14 | 15 | export interface ViewState { 16 | error: null | Error; 17 | isFetching: boolean; 18 | didInvalidate: boolean; 19 | viewsByDatabase: { 20 | [database: string]: DbTable[]; 21 | }; 22 | } 23 | 24 | const INITIAL_STATE: ViewState = { 25 | error: null, 26 | isFetching: false, 27 | didInvalidate: false, 28 | viewsByDatabase: {}, 29 | }; 30 | 31 | const viewReducer: Reducer = function ( 32 | state: ViewState = INITIAL_STATE, 33 | action, 34 | ): ViewState { 35 | switch (action.type) { 36 | case connTypes.CONNECTION_REQUEST: { 37 | return action.isServerConnection ? { ...INITIAL_STATE, didInvalidate: true } : state; 38 | } 39 | case types.FETCH_VIEWS_REQUEST: { 40 | return { 41 | ...state, 42 | isFetching: true, 43 | didInvalidate: false, 44 | error: null, 45 | }; 46 | } 47 | case types.FETCH_VIEWS_SUCCESS: { 48 | return { 49 | ...state, 50 | isFetching: false, 51 | didInvalidate: false, 52 | viewsByDatabase: { 53 | ...state.viewsByDatabase, 54 | [action.database]: action.views, 55 | }, 56 | error: null, 57 | }; 58 | } 59 | case types.FETCH_VIEWS_FAILURE: { 60 | return { 61 | ...state, 62 | isFetching: false, 63 | didInvalidate: true, 64 | error: action.error, 65 | }; 66 | } 67 | case dbTypes.REFRESH_DATABASES: { 68 | return { 69 | ...state, 70 | didInvalidate: true, 71 | }; 72 | } 73 | default: 74 | return state; 75 | } 76 | }; 77 | 78 | export default viewReducer; 79 | -------------------------------------------------------------------------------- /src/renderer/store/configure.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, Middleware } from '@reduxjs/toolkit'; 2 | import { createLogger as createReduxLogger } from 'redux-logger'; 3 | import rootReducer from '../reducers'; 4 | import { sqlectron, CONFIG } from '../api'; 5 | 6 | const middlewares: Middleware[] = []; 7 | 8 | const isLogConsoleEnabled = CONFIG.log.console; 9 | const isLogFileEnabled = CONFIG.log.file; 10 | 11 | if (isLogConsoleEnabled || isLogFileEnabled) { 12 | const loggerConfig = { 13 | level: CONFIG.log.level, 14 | collapsed: true, 15 | logger: {}, 16 | }; 17 | 18 | for (const method in console) { 19 | // eslint-disable-next-line no-console 20 | if (typeof console[method] === 'function') { 21 | // eslint-disable-line no-console 22 | loggerConfig.logger[method] = function levelFn(...args) { 23 | if (isLogConsoleEnabled) { 24 | const m = method === 'debug' ? 'log' : method; 25 | console[m](...args); // eslint-disable-line no-console 26 | } 27 | 28 | if (isLogFileEnabled) { 29 | // log on file only messages with error 30 | // otherwise is too much private information 31 | // the user would need to remove to issue a bug 32 | const lastArg = args[args.length - 1]; 33 | if (lastArg && lastArg.error) { 34 | sqlectron.logger.error('Error', lastArg.error); 35 | sqlectron.logger.error('Error Stack', lastArg.error.stack); 36 | } 37 | } 38 | }; 39 | } 40 | } 41 | 42 | middlewares.push(createReduxLogger(loggerConfig)); 43 | } 44 | 45 | export const store = configureStore({ 46 | devTools: process.env.NODE_ENV !== 'production', 47 | middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(middlewares), 48 | reducer: rootReducer, 49 | }); 50 | 51 | if (module.hot) { 52 | module.hot.accept( 53 | '../reducers', 54 | // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires 55 | () => store.replaceReducer(require('../reducers')), 56 | ); 57 | } 58 | 59 | export type RootState = ReturnType; 60 | export type AppDispatch = typeof store.dispatch; 61 | -------------------------------------------------------------------------------- /src/renderer/utils/config.ts: -------------------------------------------------------------------------------- 1 | import type { BaseConfig } from '../../common/types/config'; 2 | 3 | export function mapObjectToConfig(obj): BaseConfig { 4 | const config: BaseConfig = { 5 | zoomFactor: parseFloat(obj.zoomFactor) || 1, 6 | limitQueryDefaultSelectTop: parseInt(obj.limitQueryDefaultSelectTop, 10) || 100, 7 | enabledAutoComplete: obj.enabledAutoComplete || false, 8 | enabledLiveAutoComplete: obj.enabledLiveAutoComplete || false, 9 | enabledDarkTheme: obj.enabledDarkTheme || false, 10 | disabledOpenAnimation: obj.disabledOpenAnimation || false, 11 | csvDelimiter: obj.csvDelimiter || ',', 12 | connectionsAsList: obj.connectionsAsList || false, 13 | customFont: obj.customFont || 'Lato', 14 | }; 15 | if (!obj.log) { 16 | return config; 17 | } 18 | 19 | const { log } = obj; 20 | config.log = { 21 | console: log.console, 22 | file: log.file, 23 | level: log.level, 24 | path: log.path, 25 | }; 26 | 27 | return config; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/utils/context-menu.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | import { MenuItem } from '../../common/types/api'; 3 | 4 | export interface MenuItemSeparator { 5 | type: 'separator'; 6 | } 7 | 8 | export interface MenuItemOption { 9 | label: string; 10 | event: string; 11 | click?: () => void; 12 | } 13 | export interface MenuItemOption { 14 | label: string; 15 | event: string; 16 | click?: () => void; 17 | } 18 | 19 | function isMenuItemOption(item: MenuItemOption | MenuItemSeparator): item is MenuItemOption { 20 | return 'label' in item; 21 | } 22 | 23 | function isMenuItemSeparator(item: MenuItemOption | MenuItemSeparator): item is MenuItemSeparator { 24 | return 'type' in item; 25 | } 26 | 27 | export default class ContextMenu { 28 | menuId: string; 29 | isMenuBuilt = false; 30 | items: (MenuItemOption | MenuItemSeparator)[] = []; 31 | unsubs: Array<() => void> = []; 32 | 33 | constructor(menuId: string) { 34 | this.menuId = menuId; 35 | } 36 | 37 | append(item: MenuItemOption | MenuItemSeparator): void { 38 | if (this.isMenuBuilt) { 39 | throw new Error(`Cannot append to context menu ${this.menuId}, it is already built`); 40 | } 41 | 42 | this.items.push(item); 43 | 44 | if (isMenuItemOption(item) && item.click) { 45 | // Scope channel by menuID to avoid conflicts for multiple context menus registered at same time 46 | const eventKey = `${item.event}@${this.menuId}`; 47 | const unsub = sqlectron.browser.menu.onMenuClick(eventKey, () => { 48 | if (item.click) { 49 | item.click(); 50 | } 51 | }); 52 | 53 | this.unsubs.push(unsub); 54 | } 55 | } 56 | 57 | build(): void { 58 | if (this.isMenuBuilt) { 59 | throw new Error(`Cannot build context menu ${this.menuId}, it is already built`); 60 | } 61 | 62 | this.isMenuBuilt = true; 63 | 64 | sqlectron.browser.menu.buildContextMenu(this.menuId, { 65 | menuItems: this.items.map((item) => { 66 | // Omits the click function because it is passed by IPC to the browser process 67 | // and it cannot serialize a function. The click function is setup by the append 68 | // fucntion binding it to a event listener which then triggers the click function 69 | // when the browser process detects a click in the context menu. 70 | return { 71 | type: isMenuItemSeparator(item) ? item.type : undefined, 72 | label: isMenuItemOption(item) ? item.label : undefined, 73 | event: isMenuItemOption(item) ? item.event : undefined, 74 | }; 75 | }), 76 | }); 77 | } 78 | 79 | popup({ x, y }: { x: number; y: number }): void { 80 | if (!this.isMenuBuilt) { 81 | throw new Error(`Cannot open context menu ${this.menuId}, it is not built yet`); 82 | } 83 | 84 | sqlectron.browser.menu.popupContextMenu(this.menuId, { 85 | x, 86 | y, 87 | }); 88 | } 89 | 90 | dispose() { 91 | this.unsubs.forEach((unsub) => unsub()); 92 | this.isMenuBuilt = false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/renderer/utils/file.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from './../api'; 2 | import { DialogFilter } from './../../common/types/api'; 3 | 4 | export function showSaveDialog(filters: Array): Promise { 5 | return sqlectron.browser.dialog.showSaveDialog(filters); 6 | } 7 | 8 | export function saveFile(filename: string, data: unknown, encoding?: string): Promise { 9 | return sqlectron.browser.fs.saveFile(filename, data, encoding); 10 | } 11 | 12 | export function showOpenDialog( 13 | filters: Array, 14 | defaultPath?: string, 15 | ): Promise { 16 | return sqlectron.browser.dialog.showOpenDialog(filters, defaultPath); 17 | } 18 | 19 | export function openFile(filename: string): Promise { 20 | return sqlectron.browser.fs.openFile(filename); 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/utils/menu.ts: -------------------------------------------------------------------------------- 1 | import { sqlectron } from '../api'; 2 | 3 | export type MenuCommands = { 4 | [command: string]: () => void; 5 | }; 6 | 7 | export default class MenuHandler { 8 | commands: MenuCommands = {}; 9 | unsubs: Array<() => void> = []; 10 | 11 | setMenus(commands: MenuCommands) { 12 | if (this.commands) { 13 | this.dispose(); 14 | } 15 | 16 | const { onMenuClick } = sqlectron.browser.menu; 17 | 18 | this.unsubs = Object.keys(commands).map((command) => onMenuClick(command, commands[command])); 19 | 20 | this.commands = commands; 21 | } 22 | 23 | dispose() { 24 | this.unsubs.forEach((unsub) => unsub()); 25 | 26 | this.commands = {}; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export default function wait(time: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, time)); 3 | } 4 | -------------------------------------------------------------------------------- /stories/components/checkbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { Meta } from '@storybook/react'; 3 | 4 | import Checkbox, { CheckboxProps } from '../../src/renderer/components/checkbox'; 5 | 6 | export default { 7 | title: 'Components/Checkbox', 8 | component: Checkbox, 9 | argTypes: { 10 | name: { 11 | control: { 12 | type: 'text', 13 | }, 14 | defaultValue: 'test', 15 | }, 16 | label: { 17 | control: { 18 | type: 'text', 19 | }, 20 | defaultValue: 'label', 21 | }, 22 | disabled: { 23 | control: { 24 | type: 'boolean', 25 | }, 26 | }, 27 | checked: { 28 | control: { 29 | type: 'boolean', 30 | }, 31 | }, 32 | onChecked: { 33 | action: 'onChecked', 34 | }, 35 | onUnchecked: { 36 | action: 'onUnchecked', 37 | }, 38 | }, 39 | } as Meta; 40 | 41 | export const Primary: FC = (args) => ; 42 | -------------------------------------------------------------------------------- /test/browser/test.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { expect } from 'chai'; 3 | import { config } from '../../src/browser/core'; 4 | import { readJSONFile } from '../../src/browser/core/utils'; 5 | import { decrypt } from '../../src/browser/core/crypto'; 6 | import { EncryptedPassword } from '../../src/common/types/server'; 7 | import utilsStub from './utils-stub'; 8 | import { ConfigFile } from '../../src/common/types/config'; 9 | 10 | const cryptoSecret = 'CHK`Ya91Hs{me!^8ndwPPaPPxwQ}`'; 11 | 12 | describe('config', () => { 13 | utilsStub.getConfigPath.install({ copyFixtureToTemp: true }); 14 | 15 | describe('.prepare', () => { 16 | it('should include id for those servers without it', async () => { 17 | const findItem = (data) => data.servers.find((srv) => srv.name === 'without-id'); 18 | 19 | const fixtureBefore = await loadConfig(); 20 | await config.prepare(cryptoSecret); 21 | const fixtureAfter = await loadConfig(); 22 | 23 | expect(findItem(fixtureBefore)).to.not.have.property('id'); 24 | expect(findItem(fixtureAfter)).to.have.property('id'); 25 | const expected = await readJSONFile( 26 | path.join(__dirname, '../fixtures/browser/sqlectron.prepared.json'), 27 | ); 28 | expect(fixtureAfter.servers).to.be.same.length(expected.servers.length); 29 | fixtureAfter.servers[0].id = expected.servers[0].id; 30 | for (let i = 0; i < fixtureAfter.servers.length; i++) { 31 | const expectedServer = expected.servers[i]; 32 | const actualServer = fixtureAfter.servers[i]; 33 | if (expectedServer.password) { 34 | expect(decrypt(expected.servers[i].password as EncryptedPassword, cryptoSecret)).to.equal( 35 | decrypt(actualServer.password as EncryptedPassword, cryptoSecret), 36 | ); 37 | expectedServer.password = ''; 38 | actualServer.password = ''; 39 | } 40 | 41 | if (expectedServer.ssh && expectedServer.ssh.password) { 42 | expect(decrypt(expectedServer.ssh.password as EncryptedPassword, cryptoSecret)).to.equal( 43 | decrypt(actualServer.ssh!.password as EncryptedPassword, cryptoSecret), 44 | ); 45 | expectedServer.ssh.password = ''; 46 | actualServer.ssh!.password = ''; 47 | } 48 | 49 | expect(expectedServer).to.eql(actualServer); 50 | } 51 | }); 52 | }); 53 | 54 | function loadConfig() { 55 | return readJSONFile(utilsStub.TMP_FIXTURE_PATH); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /test/browser/test.utils.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { join } from 'path'; 3 | import { getConfigPath } from '../../src/browser/core/utils'; 4 | 5 | describe('utils', () => { 6 | describe('.getConfigPath', () => { 7 | describe('use of SQLECTRON_HOME', () => { 8 | let env: NodeJS.ProcessEnv; 9 | 10 | before(() => { 11 | env = process.env; 12 | process.env = { SQLECTRON_HOME: '/path/to/env' }; 13 | }); 14 | 15 | it('should get config from process.env.SQLECTRON_HOME', () => { 16 | expect(getConfigPath()).to.be.eql( 17 | join(process.env.SQLECTRON_HOME as string, 'sqlectron.json'), 18 | ); 19 | }); 20 | 21 | after(() => { 22 | process.env = env; 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/browser/utils-stub.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import sinon from 'sinon'; 3 | import * as utils from '../../src/browser/core/utils'; 4 | 5 | const FIXTURE_PATH = join(__dirname, '../fixtures/browser/sqlectron.json'); 6 | const TMP_FIXTURE_PATH = join(__dirname, '../fixtures/browser/tmp.sqlectron.json'); 7 | 8 | /* eslint func-names: 0 */ 9 | export default { 10 | TMP_FIXTURE_PATH, 11 | 12 | getConfigPath: { 13 | install({ copyFixtureToTemp }: { copyFixtureToTemp?: boolean }): void { 14 | const sandbox = sinon.createSandbox(); 15 | 16 | beforeEach(async () => { 17 | if (copyFixtureToTemp) { 18 | const data = await utils.readJSONFile(FIXTURE_PATH); 19 | await utils.writeJSONFile(TMP_FIXTURE_PATH, data); 20 | } 21 | sandbox.stub(utils, 'getConfigPath').returns(TMP_FIXTURE_PATH); 22 | }); 23 | 24 | afterEach(() => sandbox.restore()); 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /test/common/utils/test.string.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { titlize } from '../../../src/common/utils/string'; 3 | 4 | describe('titlize', () => { 5 | [ 6 | ['test', 'Test'], 7 | ['TEST', 'Test'], 8 | ['Test', 'Test'], 9 | ].forEach(([input, expected]) => { 10 | it(`should convert ${input} to ${expected}`, () => { 11 | expect(titlize(input)).to.equal(expected); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/e2e/helper.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { expect } from 'chai'; 3 | import { _electron as electron, ElementHandle } from 'playwright'; 4 | import type { ElectronApplication, Page } from 'playwright'; 5 | 6 | const startApp = async ({ 7 | sqlectronHome, 8 | }: { 9 | sqlectronHome: string; 10 | }): Promise<{ app: ElectronApplication; mainWindow: Page }> => { 11 | // Start Electron application 12 | const app: ElectronApplication = await electron.launch({ 13 | args: 14 | process.env.DEV_MODE === 'true' 15 | ? [path.join(__dirname, '../../src/browser/main'), '--dev'] 16 | : [path.join(__dirname, '../../out/browser/main')], 17 | // MUST pass along the host env variables, otherwise it will 18 | // crash if we use a custom env variable with tests running with xvfb 19 | env: { 20 | ...process.env, 21 | SQLECTRON_HOME: sqlectronHome, 22 | }, 23 | }); 24 | 25 | const mainWindow = await getAppPage(app); 26 | 27 | return { app, mainWindow }; 28 | }; 29 | 30 | const endApp = async (app: ElectronApplication): Promise => { 31 | // After each test close Electron application. 32 | await app.close(); 33 | }; 34 | 35 | const wait = (time: number): Promise => new Promise((resolve) => setTimeout(resolve, time)); 36 | 37 | const getAppPage = async (app: ElectronApplication, { waitAppLoad = true } = {}): Promise => { 38 | // Attempt though 25 times waiting 1s between each attempt 39 | // to get the application page 40 | for (let attempt = 0; attempt < 25; attempt++) { 41 | const windows = app.windows(); 42 | 43 | for (let i = 0; i < windows.length; i++) { 44 | const page = windows[i]; 45 | if ((await page.title()) === 'Sqlectron') { 46 | if (waitAppLoad) { 47 | // Wait until the app finished loading 48 | await page.waitForSelector('.ui'); 49 | } 50 | 51 | return page; 52 | } 53 | } 54 | 55 | await wait(1000); 56 | } 57 | 58 | throw new Error('Could not find application page'); 59 | }; 60 | 61 | const expectToEqualText = async (page: Page, selector: string, text: string): Promise => { 62 | expect(await page.$eval(selector, (node: HTMLElement) => node.innerText)).to.be.equal(text); 63 | }; 64 | 65 | const getElementByText = async ( 66 | page: Page, 67 | selector: string, 68 | text: string, 69 | ): Promise> => { 70 | const elements = await page.$$(selector); 71 | expect(elements).to.have.lengthOf.at.least(1); 72 | 73 | for (const element of elements) { 74 | const eltext = await element.innerText(); 75 | if (eltext === text) { 76 | return element as ElementHandle; 77 | } 78 | } 79 | 80 | throw new Error(`Not found element with text "${text}" for selector "${selector}"`); 81 | }; 82 | 83 | export default { 84 | startApp, 85 | endApp, 86 | wait, 87 | getAppPage, 88 | expectToEqualText, 89 | getElementByText, 90 | }; 91 | -------------------------------------------------------------------------------- /test/e2e/test.mainWindow.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { expect } from 'chai'; 4 | 5 | import helper from './helper'; 6 | import type { ElectronApplication, Page } from 'playwright'; 7 | 8 | describe('MainWindow', function () { 9 | let app: ElectronApplication; 10 | let mainWindow: Page; 11 | 12 | before(async () => { 13 | // Makes a copy of the file, because the app writes to it during the startup 14 | // which has a slight different format than we use with prettier and it causes 15 | // an unecessary change to be commited everytime the test runs. 16 | fs.copyFileSync( 17 | path.join(__dirname, '../fixtures/simple/sqlectron-sample.json'), 18 | path.join(__dirname, '../fixtures/simple/sqlectron.json'), 19 | ); 20 | 21 | const res = await helper.startApp({ 22 | sqlectronHome: path.join(__dirname, '../fixtures/simple'), 23 | }); 24 | 25 | app = res.app; 26 | mainWindow = res.mainWindow; 27 | }); 28 | 29 | after(async () => { 30 | await helper.endApp(app); 31 | }); 32 | 33 | it('script application', async () => { 34 | const appPath = await app.evaluate(({ app }) => { 35 | // This runs in the main Electron process, first parameter is 36 | // the result of the require('electron') in the main app script. 37 | return app.getAppPath(); 38 | }); 39 | 40 | if (process.env.DEV_MODE === 'true') { 41 | expect(appPath).to.be.equal(path.join(__dirname, '../../src/browser')); 42 | } else { 43 | expect(appPath).to.be.equal(path.join(__dirname, '../../out/browser')); 44 | } 45 | }); 46 | 47 | it('load servers from configuration file', async () => { 48 | await mainWindow.waitForSelector('#server-list'); 49 | 50 | const list = await mainWindow.$$('#server-list .header'); 51 | expect(list).to.have.lengthOf(1); 52 | 53 | await helper.expectToEqualText(mainWindow, '#server-list .header', 'sqlectron-local-dev'); 54 | await helper.expectToEqualText(mainWindow, '#server-list .meta', 'localhost:3306'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/fixtures/simple/sqlectron-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "console": true, 4 | "file": false, 5 | "level": "debug", 6 | "path": "sqlectron.log" 7 | }, 8 | "zoomFactor": 1, 9 | "limitQueryDefaultSelectTop": 101, 10 | "servers": [ 11 | { 12 | "id": "8d47c5fd-12af-43e3-bd0f-cdcc2eb1547c", 13 | "name": "sqlectron-local-dev", 14 | "client": "mysql", 15 | "ssl": false, 16 | "host": "localhost", 17 | "port": 3306, 18 | "socketPath": null, 19 | "user": "root", 20 | "password": { 21 | "ivText": "caW1dt5Hh5gzUn23Pk2tfQ==", 22 | "encryptedText": "DiOGAxi3k/iVGvTS377Dgw==" 23 | }, 24 | "database": "authentication", 25 | "schema": null, 26 | "encrypted": true 27 | } 28 | ], 29 | "enabledAutoComplete": true, 30 | "enabledLiveAutoComplete": false, 31 | "queries": {}, 32 | "enabledDarkTheme": false, 33 | "disabledOpenAnimation": false, 34 | "csvDelimiter": ",", 35 | "connectionsAsList": false, 36 | "customFont": "Lato" 37 | } 38 | -------------------------------------------------------------------------------- /test/fixtures/sqlite/sample-sqlectron.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "console": true, 4 | "file": false, 5 | "level": "debug", 6 | "path": "sqlectron.log" 7 | }, 8 | "zoomFactor": 1, 9 | "limitQueryDefaultSelectTop": 101, 10 | "servers": [ 11 | { 12 | "id": "4049d963-e198-489f-a0d8-a083f0a57102", 13 | "name": "sqlite-test", 14 | "client": "sqlite", 15 | "ssl": false, 16 | "host": null, 17 | "socketPath": null, 18 | "user": null, 19 | "password": null, 20 | "database": "fixtures/sqlite/test.db", 21 | "schema": null, 22 | "encrypted": true 23 | } 24 | ], 25 | "enabledAutoComplete": true, 26 | "enabledLiveAutoComplete": false, 27 | "queries": {}, 28 | "enabledDarkTheme": false, 29 | "disabledOpenAnimation": false, 30 | "csvDelimiter": ",", 31 | "connectionsAsList": false, 32 | "customFont": "Lato" 33 | } 34 | -------------------------------------------------------------------------------- /test/fixtures/ssh-mysql/sample-sqlectron.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "console": true, 4 | "file": false, 5 | "level": "debug", 6 | "path": "sqlectron.log" 7 | }, 8 | "zoomFactor": 1, 9 | "limitQueryDefaultSelectTop": 101, 10 | "servers": [ 11 | { 12 | "name": "test mysql with SSH private key rsa", 13 | "client": "mysql", 14 | "ssl": false, 15 | "host": "mysql", 16 | "port": 3306, 17 | "socketPath": null, 18 | "user": "root", 19 | "password": { 20 | "ivText": "xcNXUOBZUVANxc2l4PPKUQ==", 21 | "encryptedText": "jdcJLn8pQjJBk4KBuxIqeA==" 22 | }, 23 | "schema": null, 24 | "ssh": { 25 | "host": "127.0.0.1", 26 | "port": 2222, 27 | "user": "sqlectron", 28 | "password": null, 29 | "privateKey": "{{SQLECTRON_DB_CORE_PATH}}/spec/ssh_files/id_rsa", 30 | "useAgent": false, 31 | "privateKeyWithPassphrase": false 32 | }, 33 | "id": "6fd8427e-162d-4c98-bb3a-e2782a326617", 34 | "encrypted": true 35 | } 36 | ], 37 | "enabledAutoComplete": true, 38 | "enabledLiveAutoComplete": false, 39 | "queries": {}, 40 | "enabledDarkTheme": false, 41 | "disabledOpenAnimation": false, 42 | "csvDelimiter": ",", 43 | "connectionsAsList": false, 44 | "customFont": "Lato" 45 | } 46 | -------------------------------------------------------------------------------- /test/renderer/components/test.checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { expect } from 'chai'; 4 | 5 | import Checkbox from '../../../src/renderer/components/checkbox'; 6 | 7 | const emptyFunc = () => { 8 | /* pass */ 9 | }; 10 | 11 | describe('', () => { 12 | it('should have input with name and label elements', () => { 13 | const wrapper = shallow( 14 | , 21 | ); 22 | expect(wrapper.find('input')).to.have.length(1); 23 | expect(wrapper.find('input').props().name).to.eql('test-name'); 24 | expect(wrapper.find('label')).to.have.length(1); 25 | expect(wrapper.find('label').props().children).to.eql('test-label'); 26 | }); 27 | 28 | it('should be checked', () => { 29 | const wrapper = shallow( 30 | , 37 | ); 38 | expect(wrapper.find('input')).to.have.length(1); 39 | const input = wrapper.find('input'); 40 | expect(input.props().checked).to.be.true; 41 | }); 42 | 43 | it('should be unchecked', () => { 44 | const wrapper = shallow( 45 | , 52 | ); 53 | const input = wrapper.find('input'); 54 | expect(input.props().checked).to.be.false; 55 | }); 56 | 57 | it('should trigger onChecked for checked event', () => { 58 | let checked = false; 59 | let unchecked = false; 60 | const wrapper = shallow( 61 | { 66 | checked = true; 67 | }} 68 | onUnchecked={() => { 69 | unchecked = true; 70 | }} 71 | />, 72 | ); 73 | const input = wrapper.find('input'); 74 | expect(input.props().checked).to.be.false; 75 | input.simulate('change', { target: { checked: true } }); 76 | expect(checked).to.be.true; 77 | expect(unchecked).to.be.false; 78 | }); 79 | 80 | it('should trigger onUnchecked for unchecked event', () => { 81 | let checked = false; 82 | let unchecked = false; 83 | const wrapper = shallow( 84 | { 89 | checked = true; 90 | }} 91 | onUnchecked={() => { 92 | unchecked = true; 93 | }} 94 | />, 95 | ); 96 | const input = wrapper.find('input'); 97 | expect(input.props().checked).to.be.false; 98 | input.simulate('change', { target: { checked: false } }); 99 | expect(checked).to.be.false; 100 | expect(unchecked).to.be.true; 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import { JSDOM } from 'jsdom'; 2 | import Enzyme from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | 7 | const jsdom = new JSDOM(''); 8 | const { window } = jsdom; 9 | 10 | function copyProps(src, target) { 11 | Object.defineProperties(target, { 12 | ...Object.getOwnPropertyDescriptors(src), 13 | ...Object.getOwnPropertyDescriptors(target), 14 | }); 15 | } 16 | 17 | (global as any).window = window; 18 | global.document = window.document; 19 | global.navigator = { 20 | userAgent: 'node.js', 21 | } as Navigator; 22 | global.requestAnimationFrame = function (callback) { 23 | return setTimeout(callback, 0); 24 | }; 25 | global.cancelAnimationFrame = function (id) { 26 | clearTimeout(id); 27 | }; 28 | copyProps(window, global); 29 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "rootDir": "src", 6 | "outDir": "out", 7 | "jsx": "react", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": false, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src"] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "rootDir": ".", 6 | "outDir": "out", 7 | "jsx": "react", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitAny": false, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src", "test", "stories"] 15 | } 16 | -------------------------------------------------------------------------------- /vendor/renderer/lato/fonts/Lato-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/lato/fonts/Lato-Regular.eot -------------------------------------------------------------------------------- /vendor/renderer/lato/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/lato/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /vendor/renderer/lato/fonts/Lato-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/lato/fonts/Lato-Regular.woff -------------------------------------------------------------------------------- /vendor/renderer/lato/fonts/Lato-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/lato/fonts/Lato-Regular.woff2 -------------------------------------------------------------------------------- /vendor/renderer/lato/latofonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Lato'; 3 | src: url('fonts/Lato-Regular.eot'); /* IE9 Compat Modes */ 4 | src: url('fonts/Lato-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('fonts/Lato-Regular.woff2') format('woff2'), /* Modern Browsers */ 6 | url('fonts/Lato-Regular.woff') format('woff'), /* Modern Browsers */ 7 | url('fonts/Lato-Regular.ttf') format('truetype'); 8 | font-style: normal; 9 | font-weight: normal; 10 | text-rendering: optimizeLegibility; 11 | } -------------------------------------------------------------------------------- /vendor/renderer/semantic-ui/README.md: -------------------------------------------------------------------------------- 1 | # customized semantic-ui 2 | 3 | Sqlectron uses a slightly modified version of semantic-ui. These edits are relatively minor, 4 | and allow it to play nicely with the react / local modal. 5 | 6 | Any changes can be easily found by searching for `>>> SQLECTRON CHANGE`. The edits are specifically 7 | listed below: 8 | 9 | **IMPORTANT** In case of editing semantic-ui to bring in a new version (or otherwise), these changes 10 | will have to be re-applied! 11 | 12 | ## JS 13 | 14 | The semantic-ui js has some customizations: 15 | 16 | - observeChanges method ignores changes for "Select" class 17 | 18 | ## CSS 19 | 20 | This semantic-ui css has some customizations: 21 | 22 | - Commented import of Lato fonts from Google because we use local fonts. 23 | -------------------------------------------------------------------------------- /vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.eot -------------------------------------------------------------------------------- /vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.otf -------------------------------------------------------------------------------- /vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.ttf -------------------------------------------------------------------------------- /vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.woff -------------------------------------------------------------------------------- /vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/semantic-ui/themes/default/assets/fonts/icons.woff2 -------------------------------------------------------------------------------- /vendor/renderer/semantic-ui/themes/default/assets/images/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqlectron/sqlectron-gui/b7afa9b15a728f7791d68e8c0dbb089ec4962dfb/vendor/renderer/semantic-ui/themes/default/assets/images/flags.png --------------------------------------------------------------------------------