├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md └── stale.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── app ├── actions │ └── actions.tsx ├── app.global.css ├── app.html ├── app.icns ├── appDb.js ├── components │ ├── LoadComponent.tsx │ ├── Login.tsx │ ├── mainpanel │ │ ├── QueryResults.tsx │ │ └── Tables.tsx │ ├── omnibox │ │ └── OmniBoxInput.tsx │ └── sidepanels │ │ ├── FavoritesPanel.tsx │ │ ├── InfoPanel.tsx │ │ ├── SettingsPanel.tsx │ │ └── sidePanelMolecules │ │ ├── SingleCollapsible.tsx │ │ ├── doubleCollapsible.tsx │ │ ├── menuButton.tsx │ │ └── titles.tsx ├── constants │ ├── actionTypes.tsx │ └── routes.json ├── containers │ ├── HomePage.tsx │ ├── LoginPage.tsx │ ├── Root.tsx │ ├── SidePanel.tsx │ ├── mainpanel │ │ ├── ResultsContainer.tsx │ │ └── TablesContainer.tsx │ └── omnibox │ │ └── OmniBoxContainer.tsx ├── contexts │ └── themeContext.ts ├── db.js ├── db.ts ├── dbProcess.html ├── dbProcess.js ├── index.tsx ├── main.dev.babel.js ├── main.dev.ts ├── menu.ts ├── reducers │ ├── ChangeDisplayOfSidePanel.tsx │ ├── ChangePinnedStatus.tsx │ └── themeReducer.ts ├── themes │ ├── darkTheme.ts │ ├── defaultTheme.ts │ ├── happiTheme.ts │ ├── kateTheme.ts │ ├── themes.ts │ ├── tylerTheme.ts │ └── vaderetteTheme.ts └── utils │ └── .gitkeep ├── appveyor.yml ├── babel.config.js ├── configs ├── webpack.config.base.js ├── webpack.config.eslint.js ├── webpack.config.main.prod.babel.js ├── webpack.config.renderer.dev.babel.js ├── webpack.config.renderer.dev.dll.babel.js └── webpack.config.renderer.prod.babel.js ├── internals ├── img │ ├── eslint-padded-90.png │ ├── eslint-padded.png │ ├── eslint.png │ ├── flow-padded-90.png │ ├── flow-padded.png │ ├── flow.png │ ├── jest-padded-90.png │ ├── jest-padded.png │ ├── jest.png │ ├── js-padded.png │ ├── js.png │ ├── npm.png │ ├── react-padded-90.png │ ├── react-padded.png │ ├── react-router-padded-90.png │ ├── react-router-padded.png │ ├── react-router.png │ ├── react.png │ ├── redux-padded-90.png │ ├── redux-padded.png │ ├── redux.png │ ├── webpack-padded-90.png │ ├── webpack-padded.png │ ├── webpack.png │ ├── yarn-padded-90.png │ ├── yarn-padded.png │ └── yarn.png ├── mocks │ └── fileMock.js └── scripts │ ├── CheckBuiltsExist.js │ ├── CheckNodeEnv.js │ └── CheckPortInUse.js ├── package.json ├── renovate.json ├── resources ├── icons │ ├── seeqlicon.png_128x128.png │ ├── seeqlicon.png_16x16.png │ ├── seeqlicon.png_24x24.png │ ├── seeqlicon.png_256x256.png │ ├── seeqlicon.png_32x32.png │ ├── seeqlicon.png_48x48.png │ ├── seeqlicon.png_64x64.png │ └── seeqlicon.png_96x96.png ├── seeqlicon.png ├── seeqlicon.png.icns └── seeqlicon.png.ico ├── test ├── .eslintrc ├── actions │ ├── __snapshots__ │ │ └── counter.spec.ts.snap │ └── counter.spec.ts ├── components │ ├── Counter.spec.tsx │ └── __snapshots__ │ │ └── Counter.spec.tsx.snap ├── containers │ └── CounterPage.spec.tsx ├── e2e │ ├── HomePage.e2e.ts │ └── helpers.ts ├── example.ts └── reducers │ ├── __snapshots__ │ └── counter.spec.ts.snap │ └── counter.spec.ts ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react-hot-loader/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | .eslintcache 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | # OSX 32 | .DS_Store 33 | 34 | # flow-typed 35 | flow-typed/npm/* 36 | !flow-typed/npm/module_vx.x.x.js 37 | 38 | # App packaged 39 | release 40 | app/main.prod.js 41 | app/main.prod.js.map 42 | app/renderer.prod.js 43 | app/renderer.prod.js.map 44 | app/style.css 45 | app/style.css.map 46 | dist 47 | dll 48 | main.js 49 | main.js.map 50 | 51 | .idea 52 | npm-debug.log.* 53 | .*.dockerfile 54 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # ignore stylelinrc / *rc files 2 | .stylelintrc 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | .eslintcache 28 | 29 | # Dependency directory 30 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 31 | node_modules 32 | 33 | # OSX 34 | .DS_Store 35 | 36 | # flow-typed 37 | flow-typed/npm/* 38 | !flow-typed/npm/module_vx.x.x.js 39 | 40 | # App packaged 41 | release 42 | app/main.prod.js 43 | app/main.prod.js.map 44 | app/renderer.prod.js 45 | app/renderer.prod.js.map 46 | app/style.css 47 | app/style.css.map 48 | dist 49 | dll 50 | main.js 51 | main.js.map 52 | 53 | .idea 54 | npm-debug.log.* 55 | __snapshots__ 56 | 57 | # Package.json 58 | package.json 59 | .travis.yml 60 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', 5 | 'prettier', 6 | 'prettier/@typescript-eslint' 7 | ], 8 | parserOptions: { 9 | jsx: true, 10 | useJSXTextNode: true 11 | }, 12 | //plugins: ["@typescript-eslint", "react", "prettier"], 13 | plugins: ['@typescript-eslint'], 14 | settings: { 15 | react: { 16 | version: require('./package.json').dependencies.react 17 | } 18 | }, 19 | rules: { 20 | // let prettier control indentation 21 | '@typescript-eslint/indent': 'off', 22 | 23 | // these two should be turned off & resolved pre-production, intended to make development less distracting 24 | '@typescript-eslint/no-explicit-any': 'off', // allowed to use 'any' without jackson pollocking the file 25 | '@typescript-eslint/explicit-function-return-type': 'off', // function does not have a return type 26 | '@typescript-eslint/member-delimiter-style': 'off', // semicolons to delemit interface properties? who cares! 27 | '@typescript-eslint/interface-name-prefix': 'off', // its ok to have interfaces start with IAnInterface 28 | '@typescript-eslint/no-unused-vars': 'off', 29 | 'import/no-duplicates': 'off', 30 | 'import/no-unresolved': 'off' 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.png binary 3 | *.jpg binary 4 | *.jpeg binary 5 | *.ico binary 6 | *.icns binary 7 | 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Prerequisites 4 | 5 | - [ ] Using yarn 6 | - [ ] Using an up-to-date master branch 7 | - [ ] Using latest version of devtools. See [wiki for howto update](https://github.com/electron-react-boilerplate/electron-react-boilerplate/wiki/DevTools) 8 | - [ ] Link to stacktrace in a Gist (for bugs) 9 | - [ ] For issue in production release, devtools output of `DEBUG_PROD=true yarn build && yarn start` 10 | - [ ] Tried solutions mentioned in [#400](https://github.com/electron-react-boilerplate/electron-react-boilerplate/issues/400) 11 | 12 | ## Expected Behavior 13 | 14 | 15 | 16 | 17 | ## Current Behavior 18 | 19 | 20 | 21 | 22 | ## Possible Solution 23 | 24 | 25 | 26 | 27 | ## Steps to Reproduce (for bugs) 28 | 29 | 30 | 31 | 32 | 1. 33 | 34 | 2. 35 | 36 | 3. 37 | 38 | 4. 39 | 40 | ## Context 41 | 42 | 43 | 44 | 45 | 46 | ## Your Environment 47 | 48 | 49 | 50 | - Node version : 51 | - Version or Branch used : 52 | - Operating System and version : 53 | - Link to your project : 54 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | # 3 | daysUntilStale: 60 4 | # Number of days of inactivity before a stale issue is closed 5 | daysUntilClose: 7 6 | # Issues with these labels will never be considered stale 7 | exemptLabels: 8 | - pr 9 | - discussion 10 | - e2e 11 | - enhancement 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. Thank you 16 | for your contributions. 17 | # Comment to post when closing a stale issue. Set to `false` to disable 18 | closeComment: false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | # 3 | logs 4 | *.log 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | .eslintcache 26 | 27 | # Dependency directory 28 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 29 | node_modules 30 | 31 | # OSX 32 | .DS_Store 33 | 34 | # flow-typed 35 | flow-typed/npm/* 36 | !flow-typed/npm/module_vx.x.x.js 37 | 38 | # App packaged 39 | release 40 | app/main.prod.js 41 | app/main.prod.js.map 42 | app/renderer.prod.js 43 | app/renderer.prod.js.map 44 | app/style.css 45 | app/style.css.map 46 | dist 47 | dll 48 | main.js 49 | main.js.map 50 | 51 | .idea 52 | npm-debug.log.* 53 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | allow_failures: 3 | - os: windows 4 | include: 5 | - os: osx 6 | language: node_js 7 | node_js: 8 | - node 9 | env: 10 | - ELECTRON_CACHE=$HOME/.cache/electron 11 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 12 | 13 | #- os: linux 14 | #language: node_js 15 | #node_js: 16 | #- node 17 | #addons: 18 | #apt: 19 | #sources: 20 | #- ubuntu-toolchain-r-test 21 | #packages: 22 | #- gcc-multilib 23 | #- g++-8 24 | #- g++-multilib 25 | #- icnsutils 26 | #- graphicsmagick 27 | #- xz-utils 28 | #- xorriso 29 | #- rpm 30 | 31 | #- os: windows 32 | #language: node_js 33 | #node_js: 34 | #- node 35 | #env: 36 | #- ELECTRON_CACHE=$HOME/.cache/electron 37 | #- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 38 | 39 | before_cache: 40 | - rm -rf $HOME/.cache/electron-builder/wine 41 | 42 | cache: 43 | yarn: true 44 | directories: 45 | - node_modules 46 | - $(npm config get prefix)/lib/node_modules 47 | #- flow-typed 48 | - $HOME/.cache/electron 49 | - $HOME/.cache/electron-builder 50 | 51 | #before_install: 52 | #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX="g++-8"; fi 53 | 54 | install: 55 | - yarn --ignore-engines 56 | # On Linux, initialize "virtual display". See before_script 57 | #- | 58 | #if [ "$TRAVIS_OS_NAME" == "linux" ]; then 59 | #/sbin/start-stop-daemon \ 60 | #--start \ 61 | #--quiet \ 62 | #--pidfile /tmp/custom_xvfb_99.pid \ 63 | #--make-pidfile \ 64 | #--background \ 65 | #--exec /usr/bin/Xvfb \ 66 | #-- :99 -ac -screen 0 1280x1024x16 67 | #else 68 | #: 69 | #fi 70 | 71 | #before_script: 72 | # On Linux, create a "virtual display". This allows browsers to work properly 73 | #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=:99.0; fi 74 | #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh -e /etc/init.d/xvfb start; fi 75 | #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sleep 3; fi 76 | 77 | script: 78 | - yarn run eslint 79 | - yarn run prettier 80 | # HACK: Temporarily ignore `yarn test` on linux 81 | # - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then yarn test; fi 82 | - yarn build-e2e 83 | - yarn test-e2e 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present C. T. Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 | 4 |

5 | SeeQL Title 6 |

7 | 8 | --- 9 | 10 | Welcome to **SeeQL (beta)**: An easy-to-use desktop application that helps you visualize your database tables (including all foreign and primary key relationships), to quickly generate complex queries. 11 | 12 | ## Getting Started 13 | 14 | #### Requirements 15 | 16 | You'll need a Postgres database to connect to. 17 | 18 | #### How to Install 19 | 20 | Beta Release 0.0.1 21 | 22 | **MacOS:** [seeql-0.0.1.dmg](https://github.com/oslabs-beta/seeql/releases/download/v0.0.1/SeeQL-0.0.1-beta.dmg) 23 | 24 | *Note:* For now, you might need to go to your security settings to allow the app run on your system to allow the application to run. 25 | 26 | Or from the **terminal**, run: 27 | 28 | ``` 29 | git clone https://github.com/oslabs-beta/seeql.git 30 | cd seeql 31 | yarn 32 | yarn run build 33 | yarn start 34 | 35 | ``` 36 | 37 | 38 | ## Features 39 | 40 | **Logging In** 41 | 42 | You have the option to log in with a `postgres://` URI connection string, or enter your database credentials individually. 43 | 44 | ![Login](https://user-images.githubusercontent.com/29069478/60288146-936d7b00-98e1-11e9-8bf3-2cffdef82ff0.gif) 45 | 46 | **Viewing Database Information** 47 | 48 | After logging in, you'll see three sections - the side panel, the input box, and the database tables section. In the tables section, when you **hover** over a primary key in a table, any references to this **primary key** in other tables will be highlighted. Similarly, if you hover over a **foreign key** in a table, its related primary key will be highlighted. 49 | 50 | Click on any table's **info** icon to view its information in the side panel. 51 | 52 | Choose the **Search** option above the input box to filter which tables will be displayed. You can **pin** tables to the top of the page for your convinience by clicking on any table's pin icon. 53 | 54 | ![finalSeeQLViewDB](https://user-images.githubusercontent.com/29069478/60296862-00d6d700-98f5-11e9-9bf5-c0e15fee21ee.gif) 55 | 56 | **Generating SQL queries & Viewing the results** 57 | 58 | You can write a **SQL SELECT query** in the SQL input box, or automatically generate a query by clicking on the rows of a table. Once your query is complete, click **execute query**. If your query has any errors, an error message will display telling you exactly where the error occured. 59 | 60 | ![finalGenerateQuery](https://user-images.githubusercontent.com/29069478/60296884-0af8d580-98f5-11e9-8d26-06cb5c58f270.gif) 61 | 62 | After clicking execute, you'll be able to see your results in the **Results** section. Clicking on a column name will sort your table data accordingly. 63 | You can filter which rows are visible by clicking the search icon next to each column name. 64 | 65 | 66 | ## Resources 67 | 68 | Built on Electron, React and Typescript 69 | 70 | **Creators:** [Kate Matthrews](http://github.com/katesmatthews), [Tyler Sayles](https://github.com/saylestyler), [Ariel Hyman](https://github.com/AHyman18), [Alice Wong](https://github.com/aliicewong) 71 | -------------------------------------------------------------------------------- /app/actions/actions.tsx: -------------------------------------------------------------------------------- 1 | import * as actions from '../constants/actionTypes'; 2 | 3 | export const removeFromPinned = table_name => ({ 4 | type: actions.REMOVE_FROM_PINNED, 5 | payload: { 6 | tablename: table_name 7 | } 8 | }); 9 | 10 | export const addToPinned = table_name => ({ 11 | type: actions.ADD_TO_PINNED, 12 | payload: { 13 | tablename: table_name 14 | }}); 15 | 16 | export const changeToInfoPanel = () => ({ 17 | type: actions.CHANGE_TO_INFO_PANEL 18 | }); 19 | 20 | export const changeToFavPanel = () => ({ 21 | type: actions.CHANGE_TO_FAV_PANEL 22 | }); 23 | 24 | export const changeToSettingsPanel = () => ({ 25 | type: actions.CHANGE_TO_SETTINGS_PANEL 26 | }); 27 | -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | /* 2 | * @NOTE: Prepend a `~` to css file paths that are in your node_modules 3 | * See https://github.com/webpack-contrib/sass-loader#imports 4 | */ 5 | @import '~@fortawesome/fontawesome-free/css/all.css'; 6 | 7 | * { 8 | box-sizing: border-box; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | body { 14 | position: relative; 15 | overflow-y: hidden; 16 | font-family: 'Poppins', sans-serif; 17 | font-size: 14px; 18 | } 19 | 20 | button { 21 | font-family: 'Poppins', sans-serif; 22 | } 23 | 24 | a { 25 | font-family: 'Poppins', sans-serif; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SeeQL 6 | 17 | 18 | 22 | 23 | 24 | 25 |
26 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/app/app.icns -------------------------------------------------------------------------------- /app/appDb.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | const parseDataFile = async (filePath, defaults) => { 6 | try { 7 | const dataParsed = await JSON.parse(fs.readFileSync(filePath)); 8 | return dataParsed; 9 | } catch (error) { 10 | return defaults; 11 | } 12 | }; 13 | 14 | class AppDb { 15 | constructor(opts) { 16 | // use sync methods to avoid losing data (idle state etc.) 17 | const userDataPath = (electron.app || electron.remote.app).getPath( 18 | // allows both render and main process to get reference to app 19 | 'userData' 20 | ); 21 | this.path = path.join(userDataPath, opts.configName + '.json'); 22 | this.data = parseDataFile(this.path, opts.defaults); 23 | } 24 | 25 | get(key) { 26 | return this.data[key]; 27 | } 28 | 29 | set(key, val) { 30 | this.data[key] = val; 31 | return fs.writeFileSync(this.path, JSON.stringify(this.data)); 32 | } 33 | 34 | push(arr, obj) { 35 | return fs.writeFileSync(this.path, JSON.stringify(arr.push(obj))); 36 | } 37 | } 38 | 39 | export default AppDb; 40 | -------------------------------------------------------------------------------- /app/components/LoadComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { Spring } from 'react-spring/renderprops'; 4 | 5 | const Path = styled.path` 6 | display: flex; 7 | justify-content: center; 8 | stroke: black; 9 | fill: none; 10 | stroke-linecap: round; 11 | stroke-linejoin: round; 12 | stroke-width: 0.2px; 13 | `; 14 | 15 | const SeeqlWrapper = styled.div` 16 | width: 100%; 17 | display: flex; 18 | flex-direction: column; 19 | align-items: center; 20 | justify-content: center; 21 | `; 22 | const Svg = styled.svg` 23 | display: flex; 24 | 25 | justify-content: center; 26 | align-items: center; 27 | `; 28 | 29 | const LoadComponent = () => { 30 | return ( 31 | 32 | 33 | {props => ( 34 | 35 | 39 | 40 | )} 41 | 42 | 43 | ); 44 | }; 45 | 46 | export default LoadComponent; 47 | -------------------------------------------------------------------------------- /app/components/Login.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import * as React from 'react'; 3 | import styled, { keyframes } from 'styled-components'; 4 | import { useState } from 'react'; 5 | // import { ipcRenderer } from 'electron'; 6 | import { Client } from 'pg'; 7 | 8 | 9 | 10 | const getTables = (client) => { 11 | return new Promise((resolve, reject) => { 12 | client.query(`SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' ORDER BY table_name ASC`, 13 | (err, result) => { 14 | if (err) reject(err); 15 | resolve(result); 16 | } 17 | ); 18 | }); 19 | }; 20 | 21 | const getForeignKeys = (client, tableName) => { 22 | return new Promise((resolve, reject) => { 23 | client.query( 24 | `SELECT tc.table_schema, 25 | tc.constraint_name, 26 | tc.table_name, 27 | kcu.column_name, 28 | ccu.table_schema AS foreign_table_schema, 29 | ccu.table_name AS foreign_table_name, 30 | ccu.column_name AS foreign_column_name 31 | FROM information_schema.table_constraints AS tc 32 | JOIN information_schema.key_column_usage AS kcu 33 | ON tc.constraint_name = kcu.constraint_name 34 | AND tc.table_schema = kcu.table_schema 35 | JOIN information_schema.constraint_column_usage AS ccu 36 | ON ccu.constraint_name = tc.constraint_name 37 | AND ccu.table_schema = tc.table_schema 38 | WHERE tc.constraint_type = 'FOREIGN KEY' 39 | AND tc.table_name = '${tableName}'`, 40 | (err, result) => { 41 | if (err) reject(err); 42 | resolve(result.rows); 43 | } 44 | ); 45 | }); 46 | }; 47 | 48 | // #TODO: add error handling when tables lack a primary key 49 | // Relational database theory dictates that every table must have a primary key. 50 | // This rule is not enforced by PostgreSQL, but it is usually best to follow it. 51 | const getColumns = (client, tableName) => { 52 | return new Promise((resolve, reject) => { 53 | client.query( 54 | `SELECT COLUMN_NAME AS ColumnName, 55 | DATA_TYPE AS DataType, 56 | CHARACTER_MAXIMUM_LENGTH AS CharacterLength, 57 | COLUMN_DEFAULT AS DefaultValue 58 | FROM INFORMATION_SCHEMA.COLUMNS 59 | WHERE TABLE_NAME = '${tableName}'`, 60 | (err, result) => { 61 | if (err) 62 | // #TODO: give a msg that doesn't expose structure of database 63 | reject(err); 64 | resolve(result.rows); 65 | } 66 | ); 67 | }); 68 | }; 69 | 70 | const getPrimaryKey = (client, tableName) => { 71 | return new Promise((resolve, reject) => { 72 | client.query( 73 | `SELECT column_name 74 | FROM pg_constraint, information_schema.constraint_column_usage 75 | WHERE contype = 'p' 76 | AND information_schema.constraint_column_usage.table_name = '${tableName}' 77 | AND pg_constraint.conname = information_schema.constraint_column_usage.constraint_name`, 78 | (err, result) => { 79 | if (err) reject(err); 80 | resolve(result.rows[0].column_name); 81 | } 82 | ); 83 | }); 84 | }; 85 | async function composeTableData(client) { 86 | const tablesArr = []; 87 | let tableNames: any 88 | tableNames = await getTables(client); 89 | 90 | for (const table of tableNames.rows) { 91 | table.primaryKey = await getPrimaryKey(client, table.table_name); 92 | table.foreignKeys = await getForeignKeys(client, table.table_name); 93 | table.columns = await getColumns(client, table.table_name); 94 | tablesArr.push(table); 95 | } 96 | 97 | return new Promise((resolve, reject) => { 98 | if (tablesArr.length > 0) { 99 | resolve(tablesArr); 100 | } else { 101 | // #TODO: add empty state trigger 102 | reject(new Error('database empty')); 103 | } 104 | }); 105 | } 106 | 107 | const InvisibleHeader = styled.div` 108 | height: 30px; 109 | -webkit-app-region: drag; 110 | `; 111 | const LoginPageWrapper = styled.div` 112 | margin-top: -30px; 113 | display: flex; 114 | align-items: center; 115 | justify-content: center; 116 | height: 100%; 117 | width: 100%; 118 | `; 119 | const Title = styled.h1` 120 | font-size: 600%; 121 | font-weight: none; 122 | color: white; 123 | `; 124 | const Panel = styled.div` 125 | height: 100vh; 126 | width: 50vw; 127 | display: flex; 128 | justify-content: center; 129 | align-items: center; 130 | `; 131 | const funtimes = keyframes` 132 | 0%{background-position:0% 50%} 133 | 50%{background-position:100% 50%} 134 | 100%{background-position:0% 50%} 135 | `; 136 | const LeftPanel = styled(Panel)` 137 | background-color: white; 138 | display: flex; 139 | width: 100vw; 140 | height: 100vh; 141 | animation: ${funtimes} 8s ease infinite; 142 | background: linear-gradient(270deg, #49cefe, #c647bc); 143 | background-size: 400% 400%; 144 | `; 145 | const LoginContainer = styled.div` 146 | display: flex; 147 | flex-direction: column; 148 | align-items: center; 149 | background-color: white; 150 | border-radius: 3px; 151 | padding: 20px; 152 | `; 153 | const LoginTypeNavigation = styled.div` 154 | display: flex; 155 | flex-direction: row; 156 | align-items: center; 157 | justify-content: center; 158 | `; 159 | interface LoginTypeButtonProps { 160 | readonly selectedLoginType: string; 161 | readonly buttonType: string; 162 | } 163 | const LoginTypeButton = styled.button` 164 | padding: 5px; 165 | font-size: 120%; 166 | margin: 10px; 167 | background-color: transparent; 168 | display: flex; 169 | border: none; 170 | border-bottom: ${({ selectedLoginType, buttonType }) => 171 | selectedLoginType === buttonType 172 | ? '3px solid #4B70FE ' 173 | : '3px solid transparent'}; 174 | transition: 0.3s; 175 | :hover { 176 | border-bottom: 3px solid #4B70FE; 177 | cursor: pointer; 178 | } 179 | :focus { 180 | outline: none; 181 | } 182 | `; 183 | const URIConnectionContainer = styled.div` 184 | display: flex; 185 | flex-direction: column; 186 | padding: 20px; 187 | transition: all 0.2s; 188 | `; 189 | const InputLabel = styled.span` 190 | font-size: 80%; 191 | letter-spacing: 2px; 192 | color: #485360; 193 | `; 194 | interface IURIInputProps { 195 | requiredError: boolean; 196 | } 197 | const URIInput = styled.textarea` 198 | width: 200px; 199 | height: 150px; 200 | border-radius: 3px; 201 | letter-spacing: 2px; 202 | resize: none; 203 | padding: 8px; 204 | border: ${({ requiredError }) => 205 | requiredError ? '1px solid #ca333e' : '1px solid lightgrey'}; 206 | :focus { 207 | outline: none; 208 | } 209 | `; 210 | const ToggleSSL = styled.div` 211 | display: flex; 212 | justify-content: center; 213 | padding-bottom: 10px; 214 | display: flex; 215 | align-items: center; 216 | `; 217 | 218 | const LoginBtn = styled.button` 219 | padding: 8px; 220 | width: 150px; 221 | border: none; 222 | transition: 0.2s; 223 | border-radius: 3px; 224 | font-size: 120%; 225 | color: white; 226 | text-align: center; 227 | background-color: #4B70FE; 228 | transition: all 0.2s; 229 | span { 230 | cursor: pointer; 231 | display: inline-block; 232 | position: relative; 233 | transition: 0.5s; 234 | } 235 | span:after { 236 | content: ">>"; 237 | position: absolute; 238 | opacity: 0; 239 | top: 0; 240 | right: -20px; 241 | transition: 0.5s; 242 | } 243 | :hover { 244 | box-shadow: 0px 5px 10px #bdc3c7; 245 | span { 246 | padding-right: 5px; 247 | } 248 | span:after { 249 | opacity: 1; 250 | } 251 | } 252 | :focus { 253 | outline: none; 254 | } 255 | :active{ 256 | transform: translateY(3px); 257 | box-shadow: 0px 2px 10px #bdc3c7; 258 | } 259 | ` 260 | const CredentialsContainer = styled.div` 261 | display: flex; 262 | flex-direction: column; 263 | padding: 20px; 264 | transition: all 0.2s; 265 | `; 266 | const InputAndLabelWrapper = styled.div` 267 | margin: 5px 0px; 268 | display: flex; 269 | flex-direction: column; 270 | `; 271 | const CredentialsInput = styled.input` 272 | border-radius: 3px; 273 | padding: 8px; 274 | width: 200px; 275 | letter-spacing: 2px; 276 | border: ${({ requiredError }) => 277 | requiredError ? '1px solid #ca333e' : '1px solid lightgrey'}; 278 | :focus { 279 | outline: none; 280 | } 281 | `; 282 | const ConnectionErrorMessage = styled.div` 283 | background-color: #f1c7ca; 284 | width: 200px; 285 | color: #ca333e; 286 | border-radius: 3px; 287 | padding: 5px; 288 | margin: 5px; 289 | border-left: 3px solid #ca333e; 290 | font-size: 100%; 291 | transition: all 0.2s; 292 | `; 293 | const LogoutMessage = styled.div` 294 | background-color: #d5f5e3; 295 | width: 200px; 296 | color: #26a65b; 297 | border-radius: 3px; 298 | padding: 5px; 299 | margin: 5px; 300 | border-left: 3px solid #26a65b; 301 | font-size: 100%; 302 | transition: all 0.2s; 303 | `; 304 | const RequiredWarning = styled.span` 305 | color: #ca333e; 306 | font-size: 80%; 307 | transition: all 0.2s; 308 | `; 309 | 310 | const Login = ({ setTableData, setCurrentView, pgClient, setPgClient }) => { 311 | const [loginType, setLoginType] = useState('URI'); 312 | const [host, setHost] = useState({ value: '', requiredError: false }); 313 | const [port, setPort] = useState('5432'); 314 | const [username, setUsername] = useState({ value: '', requiredError: false }); 315 | const [password, setPassword] = useState({ value: '', requiredError: false }); 316 | const [database, setDatabase] = useState({ value: '', requiredError: false }); 317 | const [URI, setURI] = useState(''); 318 | const [isSSL, setSSL] = useState(false); 319 | const [requiredError, setRequiredError] = useState(false); 320 | const [connectionError, setConnectionError] = useState(false); 321 | const [loading, setLoading] = useState(false); 322 | const [loggedOutMessage, setLoggedOutMessage] = useState(''); 323 | const sendLoginURI = (): void => { 324 | if (loggedOutMessage) setLoggedOutMessage(''); 325 | const updatedPort = !port ? '5432' : port; 326 | let updatedURI; 327 | if (loginType === 'URI') updatedURI = URI; 328 | else if (loginType === 'Credentials') 329 | updatedURI = `postgres://${username.value}:${password.value}@${host.value}:${updatedPort}/${database.value}`; 330 | if (isSSL) updatedURI += '?ssl=true'; 331 | if (!updatedURI) setRequiredError(true); 332 | if (!host.value) setHost({ value: '', requiredError: true }); 333 | if (!username.value) setUsername({ value: '', requiredError: true }); 334 | if (!password.value) setPassword({ value: '', requiredError: true }); 335 | if (!database.value) setDatabase({ value: '', requiredError: true }); 336 | if ( 337 | URI || 338 | (host.value && username.value && password.value && database.value) 339 | ) { 340 | // const client = new Client(`postgres://ltdnkwnbccooem:64ad308e565b39cc070194f7fa621ae0e925339be5a1c69480ff2a4462eab4c4@ec2-54-163-226-238.compute-1.amazonaws.com:5432/ddsu160rb5t7vq?ssl=true`) 341 | const client = new Client(updatedURI); 342 | client.connect(); 343 | setPgClient(client) 344 | composeTableData(client) 345 | .then(tableData => { 346 | setTableData(tableData) 347 | setCurrentView('homePage') 348 | }) 349 | } 350 | }; 351 | 352 | const captureURI = (e): void => { 353 | const sanitizedURI = e.target.value.replace(/\s+/g, ''); 354 | setURI(sanitizedURI); 355 | if (requiredError) setRequiredError(false); 356 | }; 357 | return ( 358 | 359 | 360 | 361 | 362 | 363 | SeeQL 364 | 365 | 366 | 367 | {loggedOutMessage === 'inactivity' && ( 368 | 369 | You've been logged out due to inactivity. Please re-enter your credentials to login. 370 | 371 | )} 372 | {loggedOutMessage === 'userlogout' && ( 373 | You have successfully logged out. Have a nice day. 374 | )} 375 | {connectionError && ( 376 | 377 | We were unable to connect to your database. Please try again. 378 | 379 | )} 380 | 381 | { 385 | setLoginType('URI'), setConnectionError(false); 386 | }} 387 | > 388 | URI 389 | 390 | { 394 | setLoginType('Credentials'), setConnectionError(false); 395 | }} 396 | > 397 | Credentials 398 | 399 | 400 | {loginType === 'Credentials' && ( 401 | 402 | 403 | Host 404 | 410 | setHost({ value: e.target.value, requiredError: false }) 411 | } 412 | /> 413 | {host.requiredError && ( 414 | host is required 415 | )} 416 | 417 | 418 | Port 419 | setPort(e.target.value)} 425 | /> 426 | 427 | 428 | Username 429 | 435 | setUsername({ 436 | value: e.target.value, 437 | requiredError: false 438 | }) 439 | } 440 | /> 441 | {username.requiredError && ( 442 | username is required 443 | )} 444 | 445 | 446 | Password 447 | 453 | setPassword({ 454 | value: e.target.value, 455 | requiredError: false 456 | }) 457 | } 458 | /> 459 | {password.requiredError && ( 460 | password is required 461 | )} 462 | 463 | 464 | Database 465 | 471 | setDatabase({ 472 | value: e.target.value, 473 | requiredError: false 474 | }) 475 | } 476 | /> 477 | {database.requiredError && ( 478 | database is required 479 | )} 480 | 481 | 482 | )} 483 | {loginType === 'URI' && ( 484 | 485 | URI Connection String 486 | 492 | {requiredError && ( 493 | URI is required 494 | )} 495 | 496 | )} 497 | 498 | setSSL(e.target.checked)} /> 499 | ssl? 500 | 501 | {!loading && <>Login} 502 | {loading && Loading...} 503 | 504 | 505 | 506 | 507 | 508 | ); 509 | }; 510 | 511 | export default Login; 512 | -------------------------------------------------------------------------------- /app/components/mainpanel/QueryResults.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { DataTable } from "grommet"; 4 | 5 | const QueryResultWrapper = styled.div` 6 | width: 100%; 7 | border-radius: 3px; 8 | overflow: scroll; 9 | height: 100%; 10 | padding: 10px 0px 0px 0px; 11 | overflow: scroll; 12 | `; 13 | 14 | const SQueryEmptyState = styled.div` 15 | width: 100%; 16 | height: 100%; 17 | display: flex; 18 | align-items: center; 19 | justify-content: center; 20 | font-size: 120%; 21 | padding: 20px; 22 | text-align: center; 23 | 24 | ` 25 | 26 | const SResultsWrapper = styled.div` 27 | display: flex; 28 | justify-content: center; 29 | font-size: 120%; 30 | overflow: scroll; 31 | 32 | ` 33 | 34 | interface IQueryResult { 35 | status: string; 36 | message: any[]; 37 | } 38 | 39 | interface IQueryResultsProps { 40 | queryResult: IQueryResult; 41 | } 42 | 43 | const QueryResults: React.SFC = ({ queryResult }) => { 44 | const columns = []; 45 | 46 | if (queryResult.message.length > 0) { 47 | const columnNames = Object.keys(queryResult.message[0]); 48 | columnNames.forEach(column => { 49 | if (column === 'id') columns.unshift({ 50 | property: column, 51 | header: column, 52 | }) 53 | else columns.push({ 54 | property: column, 55 | header: column, 56 | }); 57 | }); 58 | } 59 | 60 | return ( 61 | 62 | { 63 | queryResult.message.length > 0 && ( 64 | 65 | ({ 67 | ...c, 68 | search: true, 69 | }))} 70 | data={queryResult.message} step={20} /> 71 | 72 | ) 73 | } 74 | { 75 | queryResult.message.length === 0 && 76 | queryResult.status === 'No results' && ( 77 |
{`There were no results found for your query.`}
{`Please enter a new query.`}
78 | ) 79 | } 80 | { 81 | queryResult.message.length === 0 && 82 | queryResult.status === 'No query' && ( 83 |
{`You haven't queried anything!`}
{` Enter a query above to get started.`}
84 | ) 85 | } 86 |
87 | ); 88 | }; 89 | 90 | export default QueryResults; 91 | -------------------------------------------------------------------------------- /app/components/mainpanel/Tables.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { License, StatusGood } from 'grommet-icons'; 4 | 5 | interface ITableProps { 6 | selectedtable: string; 7 | tablename: string; 8 | } 9 | 10 | const Table = styled.div` 11 | display: flex; 12 | flex-direction: column; 13 | font-size: 60%; 14 | border-radius: 3px; 15 | transition: 0.3s; 16 | `; 17 | 18 | const TableRowsList = styled.ul` 19 | overflow: scroll; 20 | height: 150px; 21 | `; 22 | 23 | interface ITableRowProps { 24 | affected: boolean; 25 | inTheQuery: boolean; 26 | } 27 | 28 | const TableRow = styled.li` 29 | display: flex; 30 | justify-content: space-between; 31 | list-style: none; 32 | padding: 0px 3px; 33 | border: ${ (props) => props.affected ? '2px solid #26c281' : '2px solid transparent'}; 34 | transition: 0.3s; 35 | 36 | :hover { 37 | background-color: #f4f4f4; 38 | transform: scale(1.01); 39 | cursor: pointer; 40 | } 41 | `; 42 | 43 | const TableCell = styled.p` 44 | font-size: 100%; 45 | display: flex; 46 | align-items: center; 47 | `; 48 | 49 | 50 | interface IForeignKey { 51 | column_name?: string; 52 | constraint_name?: string; 53 | foreign_column_name?: string; 54 | foreign_table_name?: string; 55 | foreign_table_schema?: string; 56 | table_name?: string; 57 | table_schema?: string; 58 | } 59 | 60 | interface IPrimaryKeyAffected { 61 | primaryKeyColumn: string; 62 | primaryKeyTable: string; 63 | } 64 | 65 | interface IForeignKeysAffected { 66 | column: string; 67 | table: string; 68 | } 69 | 70 | interface IColumnsMetaData { 71 | characterlength?: string; 72 | columnname: string; 73 | datatype: string; 74 | defaultvalue: string; 75 | } 76 | 77 | interface IActiveTableInPanel { 78 | columns?: IColumnsMetaData[]; 79 | foreignKeys?: IForeignKey[]; 80 | foreignKeysOfPrimary?: any; 81 | primaryKey?: string; 82 | table_name?: string; 83 | } 84 | 85 | interface Props { 86 | key: string; 87 | tableName: string; 88 | columns: string[]; 89 | primarykey: string; 90 | foreignkeys: IForeignKey[]; 91 | primaryKeyAffected: IPrimaryKeyAffected[]; 92 | foreignKeysAffected: IForeignKeysAffected[]; 93 | activeTableInPanel: IActiveTableInPanel; 94 | selectedForQueryTables: any; 95 | captureMouseExit: () => void; 96 | captureMouseEnter: (Event) => void; 97 | captureQuerySelections: (Event) => void; 98 | } 99 | 100 | 101 | const Tables: React.SFC = ({ 102 | tableName, 103 | columns, 104 | primarykey, 105 | foreignkeys, 106 | foreignKeysAffected, 107 | primaryKeyAffected, 108 | captureMouseExit, 109 | captureMouseEnter, 110 | activeTableInPanel, 111 | captureQuerySelections, 112 | selectedForQueryTables 113 | }) => { 114 | const rows = []; 115 | 116 | for (const keys in columns) { 117 | const primaryKey: boolean = (primarykey === columns[keys]['columnname']); 118 | let affected = false; 119 | let foreignKey = false; 120 | let foreignkeyTable = ''; 121 | let foreignkeyColumn = ''; 122 | let inTheQuery = false; 123 | if (Object.keys(selectedForQueryTables).includes(tableName)) { 124 | if ( 125 | selectedForQueryTables[tableName].columns.includes( 126 | columns[keys].columnname 127 | ) 128 | ) 129 | inTheQuery = true; 130 | } 131 | 132 | if ( 133 | primaryKeyAffected[0].primaryKeyColumn === columns[keys].columnname && 134 | primaryKeyAffected[0].primaryKeyTable === tableName 135 | ) 136 | affected = true; 137 | 138 | foreignKeysAffected.forEach((option): void => { 139 | if ( 140 | option.table === tableName && 141 | option.column === columns[keys].columnname 142 | ) 143 | affected = true; 144 | }); 145 | 146 | foreignkeys.forEach((key): void => { 147 | if (key.column_name === columns[keys].columnname) { 148 | foreignKey = true; 149 | foreignkeyTable = key.foreign_table_name; 150 | foreignkeyColumn = key.foreign_column_name; 151 | } 152 | }); 153 | 154 | rows.push( 155 | 169 | 177 | {inTheQuery && ( 178 | 179 | )} 180 | {foreignKey && ( 181 | 191 | )} 192 | {primaryKey && ( 193 | 203 | )} 204 | {` ` + columns[keys]['columnname']} 205 | 206 | 214 | {columns[keys].datatype === 'character varying' 215 | ? 'varchar' 216 | : columns[keys].datatype} 217 | 218 | 219 | ); 220 | } 221 | 222 | return ( 223 | 228 | {rows} 229 |
230 | ); 231 | }; 232 | 233 | export default Tables; 234 | -------------------------------------------------------------------------------- /app/components/omnibox/OmniBoxInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const OmniBoxWrapper = styled.div` 5 | display: flex; 6 | flex-direction: column; 7 | `; 8 | 9 | const OmniBoxInputText = styled.textarea` 10 | font-family: 'Poppins', sans-serif; 11 | font-size: 90%; 12 | border: none; 13 | background-color: #f1f1f1; 14 | padding: 10px; 15 | height: 100px; 16 | letter-spacing: 2px; 17 | resize: none; 18 | width: 100%; 19 | transition: all 0.2s; 20 | :focus { 21 | outline: none; 22 | } 23 | `; 24 | 25 | const ExecuteQueryButton = styled.button` 26 | transition: all 0.2s; 27 | text-align: center; 28 | background-color: #4B70FE; 29 | color: white; 30 | padding: 8px; 31 | font-size: 100%; 32 | border-radius: 0px 0px 3px 3px; 33 | transition: 0.2s; 34 | span { 35 | cursor: pointer; 36 | display: inline-block; 37 | position: relative; 38 | transition: 0.5s; 39 | } 40 | span:after { 41 | content: ">>"; 42 | position: absolute; 43 | opacity: 0; 44 | top: 0; 45 | right: -14px; 46 | transition: 0.5s; 47 | } 48 | :hover { 49 | span { 50 | padding-right: 10px; 51 | } 52 | span:after { 53 | opacity: 1; 54 | } 55 | } 56 | :focus { 57 | outline: none; 58 | } 59 | `; 60 | 61 | interface IOmniBoxInputProps { 62 | omniBoxView: string; 63 | userInputQuery: string; 64 | loadingQueryStatus: boolean; 65 | userInputForTables: string; 66 | setUserInputQuery: (any) => any; 67 | executeQuery: (any) => any; 68 | setUserInputForTables: (any) => any; 69 | } 70 | 71 | const OmniBoxInput: React.SFC = ({ 72 | omniBoxView, 73 | setUserInputQuery, 74 | userInputQuery, 75 | executeQuery, 76 | loadingQueryStatus, 77 | setUserInputForTables, 78 | userInputForTables 79 | }) => { 80 | if (omniBoxView === 'SQL') { 81 | return ( 82 | 83 | setUserInputQuery(e.target.value)} 85 | value={userInputQuery} 86 | > 87 | 91 | {loadingQueryStatus ? 'Loading query results...' : 'Execute Query'} 92 | 93 | 94 | ); 95 | } 96 | if (omniBoxView === 'Search') { 97 | return ( 98 | setUserInputForTables(e.target.value)} 101 | value={userInputForTables} 102 | > 103 | ); 104 | } 105 | }; 106 | 107 | export default OmniBoxInput; -------------------------------------------------------------------------------- /app/components/sidepanels/FavoritesPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const PanelWrapper = styled.div` 5 | padding: 20px; 6 | display: flex; 7 | width: 250px; 8 | justify-content: center; 9 | `; 10 | 11 | const FavoritesPanel = () => { 12 | return ( 13 | 14 | Welcome to favorites, this feature is coming soon! 15 | 16 | ); 17 | }; 18 | 19 | export default FavoritesPanel; 20 | -------------------------------------------------------------------------------- /app/components/sidepanels/InfoPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | import { InformationPanel } from './sidePanelMolecules/titles' 5 | import { Grommet } from "grommet"; 6 | import { grommet } from 'grommet/themes'; 7 | import { CircleInformation, License, Pin } from 'grommet-icons'; 8 | 9 | interface ISidePanelTableWrapperProps { 10 | sidePanelVisibility: boolean; 11 | } 12 | 13 | const TitleWrapper = styled.span` 14 | display: flex; 15 | justify-content: center; 16 | align-items: center; 17 | 18 | ` 19 | 20 | const SidePanelTableListWrapper = styled.div` 21 | width: ${({ sidePanelVisibility }) => 22 | sidePanelVisibility ? '210px' : '0px'}; 23 | height: 100%; 24 | transition: width 500ms ease-in-out; 25 | overflow: scroll; 26 | transition: all 0.2s ease-in-out; 27 | `; 28 | 29 | const LabelTextWrapper = styled.div` 30 | display: flex; 31 | flex-direction: column; 32 | overflow-wrap: break-word; 33 | padding: 5px 0px; 34 | ` 35 | 36 | const InfoSection = styled.div` 37 | overflow-wrap: break-word; 38 | margin: 20px; 39 | overflow: hidden; 40 | `; 41 | 42 | const SEmptyState = styled.div` 43 | margin: 20px; 44 | font-family: 'Poppins', sans-serif; 45 | ` 46 | 47 | const Text = styled.p` 48 | font-size: 80%; 49 | font-weight: bold; 50 | font-family: 'Poppins', sans-serif; 51 | color: #485360; 52 | padding: 0px 2px; 53 | :hover { 54 | background-color: #f4f4f4; 55 | } 56 | `; 57 | 58 | const Label = styled.label` 59 | font-size: 70%; 60 | font-family: 'Poppins', sans-serif; 61 | color:#485360; 62 | font-weight: none; 63 | `; 64 | 65 | interface ISelectedTable { 66 | columns?: any[]; 67 | foreignKeys?: any[]; 68 | primaryKey?: string; 69 | table_name?: string; 70 | foreignKeysOfPrimary?: any; 71 | } 72 | 73 | const InputLabel = styled.p` 74 | font-size: 80%; 75 | letter-spacing: 2px; 76 | color: #485360; 77 | `; 78 | 79 | interface Props { 80 | activeTableInPanel: ISelectedTable; 81 | sidePanelVisibility: boolean; 82 | } 83 | 84 | const InfoPanel: React.SFC = ({ 85 | activeTableInPanel, 86 | sidePanelVisibility, 87 | }) => { 88 | const { 89 | table_name, 90 | primaryKey, 91 | foreignKeys, 92 | foreignKeysOfPrimary 93 | } = activeTableInPanel; 94 | 95 | const foreignKeyRelationships = []; 96 | const primaryKeyRelationships = []; 97 | 98 | if (foreignKeys) { 99 | foreignKeys.forEach(key => { 100 | foreignKeyRelationships.push( 101 |
  • 102 | 103 | {key.column_name} 104 | {key.foreign_table_name}({key.foreign_column_name}) 105 | 106 |
  • 107 | ); 108 | }); 109 | } 110 | 111 | for (const foreignTableOfPrimary in foreignKeysOfPrimary) { 112 | primaryKeyRelationships.push( 113 |
  • 114 | {foreignTableOfPrimary}({foreignKeysOfPrimary[foreignTableOfPrimary]}) 115 |
  • 116 | ); 117 | } 118 | 119 | return ( 120 | 121 | 122 | 123 | {Object.keys(activeTableInPanel).length > 0 ? ( 124 | 125 | 126 | 127 | {table_name} 128 | 129 | 130 | 131 | {primaryKey} 132 | 133 | 134 | {primaryKeyRelationships.length > 0 && ( 135 |
    136 | 139 |
      {primaryKeyRelationships}
    140 |
    141 | )} 142 |
    143 | 144 | {foreignKeyRelationships.length > 0 && ( 145 |
    146 | 149 |
      {foreignKeyRelationships}
    150 |
    151 | )} 152 |
    153 |
    154 | ) : ( 155 | 156 | You haven't selected a table yet, click on the in a table to see more information. 157 |
    158 | To save a table to the top of the list, click on the {` `} 159 | in a table. 162 |
    163 | )} 164 |
    165 |
    166 | ); 167 | }; 168 | 169 | export default InfoPanel; -------------------------------------------------------------------------------- /app/components/sidepanels/SettingsPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | import { SettingsHead } from './sidePanelMolecules/titles' 4 | import { CircleInformation, Pin, License } from 'grommet-icons'; 5 | 6 | 7 | const SMiddleWrapper = styled.div` 8 | height: 100%; 9 | padding: 10px; 10 | 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: space-between; 14 | ` 15 | 16 | const SWrapper = styled.div` 17 | font-size: 80%; 18 | ` 19 | 20 | const PanelWrapper = styled.div` 21 | display: flex; 22 | flex-direction: column; 23 | height: 100%; 24 | margin: 10px 20px; 25 | justify-content: space-between 26 | transition: all 0.2s ease-in-out; 27 | `; 28 | 29 | const LabelTextWrapper = styled.div` 30 | display: flex; 31 | align-items: center; 32 | overflow-wrap: break-word; 33 | padding: 5px 0px; 34 | ` 35 | 36 | const SLabelTextWrapper = styled(LabelTextWrapper)` 37 | font-weight: bold; 38 | justify-self:'center'; 39 | align-self: center; 40 | cursor: 'pointer'; 41 | transition: 0.2s; 42 | :hover { 43 | color: #4B70FE; 44 | transform: scale(1.1); 45 | } 46 | ` 47 | const InputLabel = styled.span` 48 | font-size: 80%; 49 | letter-spacing: 2px; 50 | color: #485360; 51 | `; 52 | 53 | const SInputLabel = styled(InputLabel)` 54 | text-decoration: underline; 55 | font-weight: bold; 56 | ` 57 | 58 | const SettingsPanel = () => { 59 | 60 | return ( 61 | 62 | 63 | 64 | 65 | 66 | Primary Key 67 | 68 | 69 | Foreign Key 70 | 71 | 72 | Pin a table to the top of the list 73 | 74 | 75 | View table info 76 | 77 | 78 | Features 79 | 80 | 81 | - Hover over a row to view the relationships to other tables 82 | 83 | 84 | - Click on the rows of a table to automatically generate a query 85 | 86 | 87 | - Click reset query to remove all selected rows 88 | 89 | 90 | - Use the search to find a table quickly 91 | 92 | 93 | 94 | 95 | ); 96 | }; 97 | 98 | export default SettingsPanel; 99 | -------------------------------------------------------------------------------- /app/components/sidepanels/sidePanelMolecules/SingleCollapsible.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Box, Button, Collapsible, Grommet, Text } from "grommet"; 3 | import { grommet } from "grommet/themes"; 4 | import MenuButton from './menuButton' 5 | 6 | 7 | 8 | 9 | class SingleCollapsible extends Component { 10 | state = { 11 | openMenu1: false, 12 | }; 13 | 14 | render() { 15 | const { openMenu1 } = this.state; 16 | return ( 17 | 18 | 19 | { 23 | const newOpenMenu1 = !openMenu1; 24 | this.setState({ 25 | openMenu1: newOpenMenu1 26 | }); 27 | }} 28 | /> 29 | 30 | 31 | 44 | 45 | 46 | 59 | 60 | 61 | 75 | 88 | 89 | 102 | 103 | 104 | 105 | {} 106 | 107 | 108 | 109 | 110 | ); 111 | } 112 | 113 | } 114 | 115 | 116 | 117 | 118 | export default SingleCollapsible -------------------------------------------------------------------------------- /app/components/sidepanels/sidePanelMolecules/doubleCollapsible.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useReducer, useContext, useEffect } from "react"; 2 | import { Box, Button, Collapsible, Grommet, Text } from "grommet"; 3 | import { grommet } from "grommet/themes"; 4 | import MenuButton from './menuButton' 5 | import Context from '../../../contexts/themeContext' 6 | import themeReducer from '../../../reducers/themeReducer' 7 | import { ipcRenderer } from 'electron'; 8 | 9 | 10 | const NestedCollapsible = () => { 11 | const [context, setContext] = useContext(Context) 12 | const [openMenu1, setOpenMenu1] = useState(false) 13 | const [openSubmenu1, setOpenSubmenu1] = useState(false) 14 | const [state, dispatch] = useReducer(themeReducer, context) 15 | 16 | function findCurMode(selectedMode, context) { 17 | const activeMode = context.reduce((acc, mode) => { 18 | if (mode.active) acc = mode.value 19 | return acc; 20 | }, '') 21 | const setTheme = () => { 22 | ipcRenderer.send('user-theme-selected', selectedMode); 23 | dispatch({ 24 | type: 'CHANGE_MODE', 25 | selected: selectedMode, 26 | payload: activeMode 27 | }); 28 | } 29 | return setTheme 30 | } 31 | useEffect(() => setContext(state), [state]) 32 | 33 | return ( 34 | 35 | 36 | { 41 | const newOpenMenu1 = !openMenu1; 42 | setOpenMenu1(newOpenMenu1) 43 | setOpenSubmenu1(!newOpenMenu1 ? false : openSubmenu1) 44 | }} 45 | /> 46 | 47 | 53 | setOpenSubmenu1(!openSubmenu1) 54 | } 55 | /> 56 | 57 | 74 | 91 | {} 92 | 93 | 94 | 95 | 96 | 97 | 98 | ); 99 | } 100 | export default NestedCollapsible -------------------------------------------------------------------------------- /app/components/sidepanels/sidePanelMolecules/menuButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Button, Text } from "grommet"; 3 | 4 | import { FormDown, FormNext } from "grommet-icons"; 5 | 6 | 7 | 8 | const MenuButton = ({ label, open, submenu, ...rest }) => { 9 | const Icon = open ? FormDown : FormNext; 10 | return ( 11 | 22 | ); 23 | }; 24 | 25 | export default MenuButton; -------------------------------------------------------------------------------- /app/components/sidepanels/sidePanelMolecules/titles.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Grommet, Heading } from "grommet"; 4 | import { grommet } from "grommet/themes"; 5 | 6 | 7 | export const SettingsHead = () => { 8 | return ( 9 | 10 | Help 11 | 12 | 13 | ) 14 | } 15 | 16 | export const SignOutLink = () => { 17 | return ( 18 | 19 | SignOut 20 | 21 | 22 | ) 23 | } 24 | 25 | export const InformationPanel = () => { 26 | return ( 27 | 28 | Information 29 | 30 | 31 | ) 32 | } -------------------------------------------------------------------------------- /app/constants/actionTypes.tsx: -------------------------------------------------------------------------------- 1 | export const REMOVE_FROM_PINNED = 'REMOVE_FROM_PINNED'; 2 | export const ADD_TO_PINNED = 'ADD_TO_PINNED'; 3 | export const CHANGE_TO_INFO_PANEL = 'CHANGE_TO_INFO_PANEL'; 4 | export const CHANGE_TO_FAV_PANEL = 'CHANGE_TO_FAV_PANEL'; 5 | export const CHANGE_TO_SETTINGS_PANEL = 'CHANGE_TO_SETTINGS_PANEL'; 6 | -------------------------------------------------------------------------------- /app/constants/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "LOGIN": "/", 3 | "HOMEPAGE": "/homepage" 4 | } 5 | -------------------------------------------------------------------------------- /app/containers/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState, useEffect, useReducer } from 'react'; 3 | import styled from 'styled-components'; 4 | import { Button, Grommet, Text } from 'grommet'; 5 | import { grommet } from 'grommet/themes'; 6 | import { FormPrevious, FormNext } from "grommet-icons"; 7 | import * as actions from '../actions/actions'; 8 | import changeDisplayOfSidePanel from '../reducers/ChangeDisplayOfSidePanel'; 9 | import SidePanel from './SidePanel'; 10 | import ResultsContainer from './mainpanel/ResultsContainer'; 11 | import OmniBoxContainer from '../containers/omnibox/OmniBoxContainer'; 12 | 13 | const InvisibleHeader = styled.div` 14 | height: 40px; 15 | width: 100%; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | background-color: #F7F9FD; 20 | -webkit-app-region: drag; 21 | transition: all 0.2s ease-in-out; 22 | `; 23 | 24 | const SRightHeaderWrapper = styled.div` 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | margin: 0px 5px; 29 | cursor: pointer; 30 | ` 31 | 32 | const SHomepageWrapper = styled.div` 33 | display: flex; 34 | flex-direction: column; 35 | height: 100vh; 36 | width: 100vw; 37 | transition: all 0.2s; 38 | `; 39 | 40 | interface ISRightPanelProps { 41 | sidePanelVisibility: boolean; 42 | } 43 | 44 | const SMainPanelWrapper = styled.div` 45 | display: flex; 46 | height: 100%; 47 | width: 100%; 48 | transition: all 0.2s ease-in-out; 49 | ` 50 | 51 | const SLeftPanelWrapper = styled.div` 52 | height: 100%; 53 | width: 100%; 54 | display: flex; 55 | flex-direction: column; 56 | padding: 15px 10px 15px 15px; 57 | background: #E6EAF2; 58 | transition: all 0.2s ease-in-out; 59 | ` 60 | 61 | const SRightPanelWrapper = styled.div` 62 | height: 100%; 63 | width: ${({ sidePanelVisibility }) => sidePanelVisibility ? '250px' : '0px'}; 64 | transition: all 0.2s ease-in-out; 65 | ` 66 | 67 | let relationships = {}; 68 | const alias = {}; 69 | 70 | const HomePage = ({ pgClient, tableData, setCurrentView }) => { 71 | const [omniBoxView, setOmniBoxView] = useState('SQL'); 72 | const [overThreeTablesSelected, setOverThreeTablesSelected] = useState(false); 73 | const [selectedForQueryTables, setSelectedForQueryTables] = useState({}); 74 | const [loadingQueryStatus, setLoadingQueryStatus] = useState(false); 75 | const [activeDisplayInResultsTab, setActiveDisplayInResultsTab] = useState( 76 | 'Tables' 77 | ); 78 | const [activeTableInPanel, setActiveTableInPanel] = useState({}); 79 | const [userInputForTables, setUserInputForTables] = useState(''); 80 | const [data, setData] = useState([]); // data from database 81 | const [userInputQuery, setUserInputQuery] = useState( 82 | 'SELECT * FROM [table name]' 83 | ); 84 | const [queryResult, setQueryResult] = useState({ 85 | status: 'No query', 86 | message: [] 87 | }); 88 | const [sidePanelVisibility, setSidePanelVisibility] = useState(true); 89 | const [redirectDueToInactivity, setRedirectDueToInactivity] = useState(false); 90 | 91 | const [activePanel, dispatchSidePanelDisplay] = useReducer( 92 | changeDisplayOfSidePanel, 93 | 'info' 94 | ); 95 | const [queryResultError, setQueryResultError] = useState({ 96 | status: false, 97 | message: '' 98 | }); 99 | 100 | const resetQuerySelection = () => { 101 | relationships = {}; 102 | setOverThreeTablesSelected(false) 103 | setUserInputQuery('SELECT * FROM [table name]'); 104 | setSelectedForQueryTables({}); 105 | setQueryResultError({ 106 | status: false, 107 | message: '' 108 | }); 109 | }; 110 | 111 | // Track user inactivity, logout after 15 minutes 112 | const [inactiveTime, setInactiveTime] = useState(0); 113 | const [intervalId, captureIntervalId] = useState(); 114 | 115 | const logOut = () => { 116 | setRedirectDueToInactivity(true); 117 | clearInterval(intervalId); 118 | } 119 | 120 | useEffect(() => { 121 | captureIntervalId(setInterval(() => setInactiveTime(inactiveTime => inactiveTime + 1), 60000)); 122 | return () => clearInterval(intervalId); 123 | }, []); 124 | 125 | useEffect(() => { if (inactiveTime >= 15) logOut() }, [inactiveTime]); 126 | 127 | const captureQuerySelections = e => { 128 | const selectedTableName = e.target.dataset.tablename; 129 | const selectedColumnName = e.target.dataset.columnname; 130 | let firstColumn = true; 131 | let firstTable = true; 132 | let pk = ''; 133 | const temp = selectedForQueryTables; 134 | let columns = ''; 135 | let tables = ''; 136 | let query = ''; 137 | relationships[selectedTableName] = []; 138 | 139 | // get relationships of FK 140 | data.forEach(table => { 141 | if (table.table_name === selectedTableName) { 142 | pk = table.primaryKey; 143 | table.foreignKeys.forEach(foreignkey => { 144 | relationships[selectedTableName].push({ 145 | tablename: foreignkey.table_name, 146 | colname: foreignkey.column_name, 147 | fktablename: foreignkey.foreign_table_name, 148 | fkcolname: foreignkey.foreign_column_name 149 | }); 150 | }); 151 | } 152 | }); 153 | 154 | // get relationships of PK 155 | data.forEach(table => { 156 | table.foreignKeys.forEach(foreignkey => { 157 | if ( 158 | foreignkey.foreign_column_name == pk && 159 | foreignkey.foreign_table_name == selectedTableName 160 | ) { 161 | relationships[selectedTableName].push({ 162 | tablename: foreignkey.foreign_table_name, 163 | colname: foreignkey.foreign_column_name, 164 | fktablename: foreignkey.table_name, 165 | fkcolname: foreignkey.column_name 166 | }); 167 | } 168 | }); 169 | }); 170 | 171 | // builds the object used to write the query 172 | for (let i = 0; i < data.length; i++) { 173 | if (data[i].table_name === selectedTableName) { 174 | // builds query selection object 175 | // check if table already exists in query 176 | if (Object.keys(temp).includes(selectedTableName)) { 177 | // check if column name already exists 178 | if (temp[selectedTableName].columns.includes(selectedColumnName)) { 179 | // remove the column if it exists 180 | const startIndex = temp[selectedTableName].columns.indexOf( 181 | selectedColumnName 182 | ); 183 | temp[selectedTableName].columns = temp[selectedTableName].columns 184 | .slice(0, startIndex) 185 | .concat(temp[selectedTableName].columns.slice(startIndex + 1)); 186 | // add it to the columns 187 | } else { 188 | temp[selectedTableName].columns.push(selectedColumnName); 189 | } 190 | // check if all items are selected 191 | if ( 192 | temp[selectedTableName].columns.length === 193 | temp[selectedTableName].columncount 194 | ) { 195 | temp[selectedTableName].all = true; 196 | } else { 197 | temp[selectedTableName].all = false; 198 | } 199 | // delete entire object if the columns are now empty 200 | if (temp[selectedTableName].columns.length === 0) { 201 | // if empty after removing 202 | delete temp[selectedTableName]; 203 | delete relationships[selectedTableName]; 204 | delete alias[selectedTableName]; 205 | } 206 | } else { 207 | // first row and first table to be selected 208 | temp[selectedTableName] = { 209 | all: false, 210 | columncount: data[i].columns.length, 211 | columns: [selectedColumnName] 212 | }; 213 | } 214 | } 215 | } 216 | 217 | // query generation 218 | // for no tables 219 | if (Object.keys(temp).length === 0) { 220 | query = 'SELECT * FROM[table name]'; 221 | } 222 | 223 | // for one table 224 | if (Object.keys(temp).length === 1) { 225 | for (const table in temp) { 226 | // check if all has been selected 227 | if (temp[table].all) columns += '*'; 228 | else { 229 | for (let i = 0; i < temp[table].columns.length; i++) { 230 | if (firstColumn) { 231 | columns += temp[table].columns[i]; 232 | firstColumn = false; 233 | } else columns += `, ${temp[table].columns[i]}`; 234 | } 235 | } 236 | } 237 | tables = Object.keys(temp)[0]; 238 | query = `SELECT ${columns} FROM ${tables}`; 239 | } 240 | 241 | let previousTablePointer; 242 | 243 | // for multiple joins 244 | if (Object.keys(temp).length === 2) { 245 | for (const table in temp) { 246 | // loop through each table 247 | let aliasIndex = 0; 248 | let tableInitial = table[0]; 249 | while (Object.values(alias).includes(table[aliasIndex])) { 250 | tableInitial += table[aliasIndex + 1]; 251 | aliasIndex++; // initial of each table 252 | } 253 | alias[table] = tableInitial; 254 | tableInitial += '.'; 255 | // check if all the columns have been selected 256 | if (temp[table].all) { 257 | if (firstColumn) { 258 | columns += `${tableInitial}*`; 259 | firstColumn = false; 260 | } else columns += `, ${tableInitial}*`; 261 | } else { 262 | // add each individual column name 263 | for (let i = 0; i < temp[table].columns.length; i++) { 264 | if (firstColumn) { 265 | columns += tableInitial + temp[table].columns[i]; 266 | firstColumn = false; 267 | } else { 268 | columns += `, ${tableInitial}${temp[table].columns[i]}`; 269 | } 270 | } 271 | } 272 | 273 | // create the table name 274 | if (firstTable) { 275 | tables += `${table} as ${table[0]}`; 276 | firstTable = false; 277 | } else { 278 | tables += ` INNER JOIN ${table} as ${alias[table]}`; 279 | let rel = ''; 280 | relationships[table].forEach(relation => { 281 | if ( 282 | relation.fktablename === previousTablePointer && 283 | relation.tablename === table 284 | ) { 285 | rel = 286 | `${alias[previousTablePointer] 287 | }.${ 288 | relation.fkcolname 289 | }=${ 290 | tableInitial + relation.colname}`; 291 | } 292 | }); 293 | tables += ` ON ${rel}`; 294 | } 295 | previousTablePointer = table; 296 | } 297 | 298 | // final query 299 | query = `SELECT ${columns} FROM ${tables}`; 300 | } 301 | 302 | //error handle for 3+ joins 303 | if (Object.keys(temp).length > 2) { 304 | setOverThreeTablesSelected(true) 305 | } else { 306 | setOverThreeTablesSelected(false) 307 | } 308 | 309 | 310 | setUserInputQuery(query); 311 | setSelectedForQueryTables(temp); 312 | }; 313 | 314 | const togglePanelVisibility = () => { 315 | if (sidePanelVisibility) { 316 | setSidePanelVisibility(false); 317 | setActiveTableInPanel({}); 318 | } else setSidePanelVisibility(true); 319 | }; 320 | 321 | const captureSelectedTable = e => { 322 | const { tablename } = e.target.dataset; 323 | let selectedPanelInfo = {}; 324 | let primaryKey; 325 | 326 | data.forEach(table => { 327 | if (table.table_name === tablename) { 328 | primaryKey = table.primaryKey; 329 | selectedPanelInfo = table; 330 | } 331 | }); 332 | 333 | selectedPanelInfo.foreignKeysOfPrimary = {}; 334 | 335 | data.forEach(table => { 336 | table.foreignKeys.forEach(foreignKey => { 337 | if ( 338 | foreignKey.foreign_column_name == primaryKey && 339 | foreignKey.foreign_table_name == tablename 340 | ) { 341 | selectedPanelInfo.foreignKeysOfPrimary[foreignKey.table_name] = 342 | foreignKey.column_name; 343 | } 344 | }); 345 | }); 346 | setActiveTableInPanel('info'); 347 | setSidePanelVisibility(true); 348 | setActiveTableInPanel(selectedPanelInfo); 349 | dispatchSidePanelDisplay(actions.changeToInfoPanel()); 350 | }; 351 | 352 | // Fetches database information 353 | useEffect((): void => { 354 | setData(tableData); 355 | }, [tableData]); 356 | 357 | useEffect(() => { 358 | if (queryResult.statusCode === 'Success') { 359 | setQueryResult({ 360 | status: queryResult.message.length === 0 ? 'No results' : 'Success', 361 | message: queryResult.message 362 | }); 363 | setActiveDisplayInResultsTab('Query Results'); 364 | } 365 | if (queryResult.statusCode === 'Invalid Request') { 366 | setQueryResultError({ 367 | status: true, 368 | message: queryResult.message 369 | }); 370 | } 371 | if (queryResult.statusCode === 'Syntax Error') { 372 | setQueryResultError({ 373 | status: true, 374 | message: `Syntax error in retrieving query results. 375 | Error on: ${userInputQuery.slice( 376 | 0, 377 | parseInt(queryResult.err.position) - 1 378 | )} " 379 | ${userInputQuery.slice( 380 | parseInt(queryResult.err.position) - 1, 381 | parseInt(queryResult.err.position) 382 | )} " 383 | ${userInputQuery.slice(parseInt(queryResult.err.position))};` 384 | }); 385 | } 386 | setLoadingQueryStatus(false); 387 | }, [queryResult]); 388 | 389 | return ( 390 | 391 | 392 | {redirectDueToInactivity && setCurrentView('loginPage')} 393 | setInactiveTime(0)}> 394 | 395 |
    396 | 397 | Menu 398 | 38 | 48 | 56 | 64 | 65 | 66 | 67 | `; 68 | -------------------------------------------------------------------------------- /test/containers/CounterPage.spec.tsx: -------------------------------------------------------------------------------- 1 | // import * as React from 'react'; 2 | // import Enzyme, { mount } from 'enzyme'; 3 | // import Adapter from 'enzyme-adapter-react-16'; 4 | // import { Provider } from 'react-redux'; 5 | // import { createBrowserHistory } from 'history'; 6 | // import { ConnectedRouter } from 'connected-react-router'; 7 | // // import CounterPage from '../../app/containers/CounterPage'; 8 | // import { configureStore } from '../../app/store/configureStore'; 9 | 10 | // Enzyme.configure({ adapter: new Adapter() }); 11 | 12 | // function setup(initialState?: any) { 13 | // const store = configureStore(initialState); 14 | // const history = createBrowserHistory(); 15 | // const provider = ( 16 | // 17 | // 18 | // 19 | // 20 | // 21 | // ); 22 | // const app = mount(provider); 23 | // return { 24 | // app, 25 | // buttons: app.find('button'), 26 | // p: app.find('.counter') 27 | // }; 28 | // } 29 | 30 | // describe('containers', () => { 31 | // describe('App', () => { 32 | // it('should display initial count', () => { 33 | // const { p } = setup(); 34 | // expect(p.text()).toMatch(/^0$/); 35 | // }); 36 | 37 | // it('should display updated count after increment button click', () => { 38 | // const { buttons, p } = setup(); 39 | // buttons.at(0).simulate('click'); 40 | // expect(p.text()).toMatch(/^1$/); 41 | // }); 42 | 43 | // it('should display updated count after decrement button click', () => { 44 | // const { buttons, p } = setup(); 45 | // buttons.at(1).simulate('click'); 46 | // expect(p.text()).toMatch(/^-1$/); 47 | // }); 48 | 49 | // it('shouldnt change if even and if odd button clicked', () => { 50 | // const { buttons, p } = setup(); 51 | // buttons.at(2).simulate('click'); 52 | // expect(p.text()).toMatch(/^0$/); 53 | // }); 54 | 55 | // it('should change if odd and if odd button clicked', () => { 56 | // const { buttons, p } = setup({ counter: 1 }); 57 | // buttons.at(2).simulate('click'); 58 | // expect(p.text()).toMatch(/^2$/); 59 | // }); 60 | // }); 61 | // }); 62 | -------------------------------------------------------------------------------- /test/e2e/HomePage.e2e.ts: -------------------------------------------------------------------------------- 1 | import { ClientFunction, Selector } from 'testcafe'; 2 | import { ReactSelector, waitForReact } from 'testcafe-react-selectors'; 3 | import { getPageUrl } from './helpers'; 4 | 5 | const getPageTitle = ClientFunction(() => document.title); 6 | const counterSelector = Selector('[data-tid="counter"]'); 7 | const buttonsSelector = Selector('[data-tclass="btn"]'); 8 | const clickToCounterLink = t => 9 | t.click(Selector('a').withExactText('to Counter')); 10 | const incrementButton = buttonsSelector.nth(0); 11 | const decrementButton = buttonsSelector.nth(1); 12 | const oddButton = buttonsSelector.nth(2); 13 | const asyncButton = buttonsSelector.nth(3); 14 | const getCounterText = () => counterSelector().innerText; 15 | const assertNoConsoleErrors = async t => { 16 | const { error } = await t.getBrowserConsoleMessages(); 17 | await t.expect(error).eql([]); 18 | }; 19 | 20 | fixture`Home Page`.page('../../app/app.html').afterEach(assertNoConsoleErrors); 21 | 22 | test('e2e', async t => { 23 | await t.expect(getPageTitle()).eql('Hello Electron React!'); 24 | }); 25 | 26 | test('should open window', async t => { 27 | await t.expect(getPageTitle()).eql('Hello Electron React!'); 28 | }); 29 | 30 | test( 31 | "should haven't any logs in console of main window", 32 | assertNoConsoleErrors 33 | ); 34 | 35 | test('should to Counter with click "to Counter" link', async t => { 36 | await t 37 | .click('[data-tid=container] > a') 38 | .expect(getCounterText()) 39 | .eql('0'); 40 | }); 41 | 42 | test('should navgiate to /counter', async t => { 43 | await waitForReact(); 44 | await t 45 | .click( 46 | ReactSelector('Link').withProps({ 47 | to: '/counter' 48 | }) 49 | ) 50 | .expect(getPageUrl()) 51 | .contains('/counter'); 52 | }); 53 | 54 | fixture`Counter Tests` 55 | .page('../../app/app.html') 56 | .beforeEach(clickToCounterLink) 57 | .afterEach(assertNoConsoleErrors); 58 | 59 | test('should display updated count after increment button click', async t => { 60 | await t 61 | .click(incrementButton) 62 | .expect(getCounterText()) 63 | .eql('1'); 64 | }); 65 | 66 | test('should display updated count after descrement button click', async t => { 67 | await t 68 | .click(decrementButton) 69 | .expect(getCounterText()) 70 | .eql('-1'); 71 | }); 72 | 73 | test('should not change if even and if odd button clicked', async t => { 74 | await t 75 | .click(oddButton) 76 | .expect(getCounterText()) 77 | .eql('0'); 78 | }); 79 | 80 | test('should change if odd and if odd button clicked', async t => { 81 | await t 82 | .click(incrementButton) 83 | .click(oddButton) 84 | .expect(getCounterText()) 85 | .eql('2'); 86 | }); 87 | 88 | test('should change if async button clicked and a second later', async t => { 89 | await t 90 | .click(asyncButton) 91 | .expect(getCounterText()) 92 | .eql('0') 93 | .expect(getCounterText()) 94 | .eql('1'); 95 | }); 96 | 97 | test('should back to home if back button clicked', async t => { 98 | await t 99 | .click('[data-tid="backButton"] > a') 100 | .expect(Selector('[data-tid="container"]').visible) 101 | .ok(); 102 | }); 103 | -------------------------------------------------------------------------------- /test/e2e/helpers.ts: -------------------------------------------------------------------------------- 1 | /* eslint import/prefer-default-export: off */ 2 | import { ClientFunction } from 'testcafe'; 3 | 4 | export const getPageUrl = ClientFunction(() => window.location.href); 5 | -------------------------------------------------------------------------------- /test/example.ts: -------------------------------------------------------------------------------- 1 | describe('description', () => { 2 | it('should have description', () => { 3 | expect(1 + 2).toBe(3); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /test/reducers/__snapshots__/counter.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`reducers counter should handle DECREMENT_COUNTER 1`] = `0`; 4 | 5 | exports[`reducers counter should handle INCREMENT_COUNTER 1`] = `2`; 6 | -------------------------------------------------------------------------------- /test/reducers/counter.spec.ts: -------------------------------------------------------------------------------- 1 | // import counter from '../../app/reducers/counter'; 2 | // import { CounterTypeKeys } from '../../app/actions/counter'; 3 | 4 | // describe('reducers', () => { 5 | // describe('counter', () => { 6 | // it('should handle INCREMENT_COUNTER', () => { 7 | // expect( 8 | // counter(1, { 9 | // type: CounterTypeKeys.INCREMENT_COUNTER 10 | // }) 11 | // ).toMatchSnapshot(); 12 | // }); 13 | 14 | // it('should handle DECREMENT_COUNTER', () => { 15 | // expect( 16 | // counter(1, { 17 | // type: CounterTypeKeys.DECREMENT_COUNTER 18 | // }) 19 | // ).toMatchSnapshot(); 20 | // }); 21 | // }); 22 | // }); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "baseUrl": "./", 5 | "rootDir": "./", 6 | "jsx": "react", 7 | "module": "es2015", 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "noUnusedLocals": true, 12 | "pretty": true, 13 | "sourceMap": true, 14 | "resolveJsonModule": true, 15 | "types": ["node", "electron", "pg", "jest"], 16 | "typeRoots": ["node_modules/@types"], 17 | "lib": ["es6", "dom"], 18 | "allowJs": true, 19 | "allowSyntheticDefaultImports": true 20 | }, 21 | "exclude": ["node_modules", "**/node_modules/*"] 22 | } 23 | --------------------------------------------------------------------------------