├── .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 | [](http://makeapullrequest.com)
2 | [](https://opensource.org/licenses/MIT)
3 |
4 |
5 |
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 | 
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 | 
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 | 
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 |
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 |
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 | : }
405 | />
406 |
407 |
408 |
409 |
410 |
425 |
439 |
440 |
441 | {/* */}
442 |
450 | {/* */}
451 |
452 |
453 |
454 |
455 |
456 | );
457 | };
458 |
459 | export default HomePage;
460 |
--------------------------------------------------------------------------------
/app/containers/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Login from '../components/Login';
3 |
4 | interface Props {
5 | setTableData: (any) => any;
6 | setCurrentView: (any) => any;
7 | setPgClient: (any) => any;
8 | }
9 |
10 | const LoginPage: React.SFC = ({ setTableData, setCurrentView, setPgClient }) =>
11 | ;
16 |
17 | export default LoginPage;
--------------------------------------------------------------------------------
/app/containers/Root.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Component } from 'react';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import Routes from '../Routes';
5 |
6 | interface Props {}
7 |
8 | export default class Root extends Component {
9 | render() {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/containers/SidePanel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 | import SettingsPanel from '../components/sidepanels/SettingsPanel';
4 | import FavoritesPanel from '../components/sidepanels/FavoritesPanel';
5 | import InfoPanel from '../components/sidepanels/InfoPanel';
6 | import * as actions from '../actions/actions';
7 | import { Grommet } from "grommet";
8 | import { grommet } from 'grommet/themes';
9 | import { CircleQuestion, CircleInformation, Logout } from 'grommet-icons';
10 |
11 | interface IPanelWrapperProps {
12 | sidePanelVisibility: boolean;
13 | }
14 |
15 | interface IIndTabProps {
16 | active: string;
17 | panel: string;
18 | }
19 |
20 |
21 |
22 | const PanelWrapper = styled.div`
23 | width: ${({ sidePanelVisibility }) =>
24 | sidePanelVisibility ? '250px' : '0px'};
25 | display: flex;
26 | flex-direction: column;
27 | padding: 10px 10px 10px 0px;
28 | justify-content: flex-start;
29 | background-color: #E6EAF2;
30 | height: 100%;
31 | transition: all 0.2s ease-in-out;
32 | font-family: 'Poppins', sans-serif;
33 | `;
34 |
35 | const SInnerPanelWrapper = styled.div`
36 | margin: 5px;
37 | background-color: white;
38 | height: 100%;
39 | border-radius: 3px;
40 | box-shadow: 1px 1px 4px #67809f;
41 | display: flex;
42 | flex-direction: column;
43 | align-items: center;
44 | `;
45 |
46 | const SIndTab = styled.button`
47 | font-family: 'Poppins', sans-serif;
48 | border: none;
49 | margin: 5px;
50 | padding: 3px 0px;
51 | cursor: pointer;
52 | transition: 0.2s;
53 | border-bottom: 2px solid transparent;
54 |
55 | :hover {
56 | transform: scale(1.1);
57 | border-bottom: 2px solid #4B70FE;
58 | }
59 | :focus {
60 | outline: none;
61 | }
62 | `
63 |
64 | const IndTab = styled.button`
65 | font-family: 'Poppins', sans-serif;
66 | border: none;
67 | border-bottom: ${({ active, panel }) => active === panel ? '2px solid #4B70FE' : '2px solid transparent'};
68 | margin: 5px;
69 | padding: 3px 0px;
70 | cursor: pointer;
71 | transition: 0.2s;
72 |
73 | :hover {
74 | transform: scale(1.1);
75 | border-bottom: 2px solid #4B70FE;
76 | }
77 | :focus {
78 | outline: none;
79 | }
80 | `;
81 |
82 | const Tabs = styled.div`
83 | display: flex;
84 | margin: 10px;
85 | height: 60px;
86 | `;
87 |
88 | const SSectionWrapper = styled.div`
89 | height: 100%;
90 | overflow: scroll;
91 | `
92 |
93 | interface IForeignKeysAffected {
94 | column: string;
95 | table: string;
96 | }
97 |
98 | interface IColumnsMetaData {
99 | characterlength?: string;
100 | columnname: string;
101 | datatype: string;
102 | defaultvalue: string;
103 | }
104 |
105 | interface IAcitveTableInPanel {
106 | columns?: IColumnsMetaData[];
107 | foreignKeys?: IForeignKeysAffected[];
108 | primaryKey?: string;
109 | table_name?: string;
110 | foreignKeysOfPrimary?: any;
111 | }
112 |
113 | interface IDispatchSidePanelDisplayAction {
114 | type: string;
115 | }
116 |
117 | interface Props {
118 | intervalId: number;
119 | activeTableInPanel: IAcitveTableInPanel;
120 | sidePanelVisibility: boolean;
121 | activePanel: string;
122 | dispatchSidePanelDisplay: (IDispatchSidePanelDisplayAction) => any;
123 |
124 | setCurrentView: (any) => any;
125 | }
126 |
127 | const SidePanel: React.SFC = ({
128 | intervalId,
129 | setCurrentView,
130 | activeTableInPanel,
131 | sidePanelVisibility,
132 | activePanel,
133 | dispatchSidePanelDisplay,
134 | }) => {
135 | return (
136 |
137 | {sidePanelVisibility && (
138 |
139 |
140 |
141 |
146 | dispatchSidePanelDisplay(actions.changeToInfoPanel())
147 | }
148 | >
149 |
150 |
151 |
156 | dispatchSidePanelDisplay(actions.changeToSettingsPanel())
157 | }
158 | >
159 |
160 |
161 | {
163 | clearInterval(intervalId);
164 | setCurrentView('loginPage')
165 | }}
166 | >
167 |
170 |
171 |
172 |
173 | {activePanel === 'info' && (
174 |
178 | )}
179 | {activePanel === 'favorites' && }
180 | {activePanel === 'settings' && }
181 |
182 |
183 |
184 | )
185 | }
186 |
187 | );
188 | };
189 |
190 | export default SidePanel;
191 |
--------------------------------------------------------------------------------
/app/containers/mainpanel/ResultsContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 | import QueryResults from "../../components/mainpanel/QueryResults";
4 | import TablesContainer from './TablesContainer';
5 |
6 | const ResultsWrapper = styled.div`
7 | display: flex;
8 | flex-direction: column;
9 | align-items: flex-start;
10 | overflow: hidden;
11 | background-color: white;
12 | height: 100%;
13 | margin-top: 15px;
14 | min-width: 400px;
15 | box-shadow: 1px 1px 4px #67809f;
16 | border-radius: 3px;
17 | font-family: 'Poppins', sans-serif;
18 | `
19 |
20 | const SRestTabsRight = styled.div`
21 | display: flex;
22 | justify-content: flex-end;
23 | align-items: center;
24 | width: 50%;
25 | font-family: 'Poppins', sans-serif;
26 | `
27 |
28 | const SResNavTabs = styled.div`
29 | display: flex;
30 | align-items: center;
31 | width: 50%;
32 | font-family: 'Poppins', sans-serif;
33 | `
34 |
35 | interface SResultsNavButtonProps {
36 | activeDisplayInResultsTab: string;
37 | activetabname: string;
38 | }
39 |
40 |
41 | const SResultsNavButton = styled.button`
42 | border: none;
43 | font-size: 80%;
44 | margin: 0px 5px;
45 | font-family: 'Poppins', sans-serif;
46 | color: ${({ activeDisplayInResultsTab, activetabname }) => activeDisplayInResultsTab === activetabname ? '#4B70FE' : '#485360'};
47 | border-bottom: ${({ activeDisplayInResultsTab, activetabname }) => activeDisplayInResultsTab === activetabname ? '2px solid #4B70FE' : '2px solid transparent'};
48 | transition: all 0.2s;
49 | cursor: pointer;
50 |
51 | :hover {
52 | border-bottom: 2px solid #4B70FE;
53 | }
54 |
55 | :focus{
56 | outline: none;
57 | }
58 |
59 | `
60 |
61 | const SResetQueryButton = styled.button`
62 | border: none;
63 | font-size: 70%;
64 | cursor: pointer;
65 | transition: all 0.2s;
66 | font-family: 'Poppins', sans-serif;
67 | :hover{
68 | color: #ca333e;
69 | span {
70 | visibility: visible;
71 | }
72 |
73 | }
74 | :focus {
75 | outline: none;
76 | }
77 | span{
78 | visibility: hidden;
79 | font-weight: bold;
80 | position: relative;
81 | background-color: #fef5e7;
82 | color: #f5ab35;
83 | text-align: center;
84 | border-radius: 10px;
85 | padding: 8px
86 | top: 5px;
87 | margin: 0px 3px;
88 | transition: all 0.2s;
89 | }
90 |
91 | }
92 | `
93 |
94 | const STopNav = styled.div`
95 | width: 100%;
96 | display: flex;
97 | justify-content: space-between;
98 | align-items: center;
99 | padding: 0px 10px;
100 | min-height: 40px;
101 | border-bottom: 1px solid #dadfe1;
102 | `
103 |
104 | const TooManySelectedTablesWarning = styled.div`
105 | color: #f5ab35;
106 | font-family: 'Poppins', sans-serif;
107 | border-radius: 3px;
108 | border-left: 3px solid #f5ab35;
109 | font-size: 80%;
110 | background-color: #fef5e7;
111 | margin: 5px 0px;
112 | padding: 5px;
113 | width: 80%;
114 | align-self: center;
115 | `;
116 |
117 | interface IResultsContainerProps {
118 | activeDisplayInResultsTab: string;
119 | queryResult: any;
120 | data: any;
121 | userInputForTables: string;
122 | activeTableInPanel: any;
123 | selectedForQueryTables: any;
124 | relationships;
125 | overThreeTablesSelected: any;
126 | resetQuerySelection: (any) => any;
127 | captureQuerySelections: (any) => any;
128 | captureSelectedTable: (any) => any;
129 | setActiveDisplayInResultsTab: (any) => any;
130 | }
131 |
132 | const ResultsContainer: React.SFC = ({
133 | activeDisplayInResultsTab,
134 | queryResult,
135 | userInputForTables,
136 | activeTableInPanel,
137 | selectedForQueryTables,
138 | data,
139 | captureSelectedTable,
140 | captureQuerySelections,
141 | setActiveDisplayInResultsTab,
142 | resetQuerySelection,
143 | relationships,
144 | overThreeTablesSelected
145 | }) => {
146 |
147 | const listOfTabNames = ['Tables', 'Query Results'];
148 |
149 | const resultsTabs = listOfTabNames.map((tabname) => {
150 | return setActiveDisplayInResultsTab(tabname)}
155 | >{tabname}
156 | })
157 |
158 | return (
159 |
160 |
161 |
162 | {resultsTabs}
163 |
164 |
165 | This will remove all selected columnsReset Query
166 |
167 |
168 | {overThreeTablesSelected &&
169 | Automatic query generation only works on one or two tables. Please unselect any additional tables or reset the query.
170 | }
171 | {activeDisplayInResultsTab === 'Tables' &&
172 |
182 | }
183 | {activeDisplayInResultsTab === 'Query Results' && (
184 |
185 | )}
186 |
187 | )
188 | }
189 |
190 | export default ResultsContainer;
--------------------------------------------------------------------------------
/app/containers/mainpanel/TablesContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useState, useEffect, useReducer } from 'react';
3 | import styled from 'styled-components';
4 | import * as actions from '../../actions/actions';
5 | import Tables from '../../components/mainpanel/Tables';
6 | import changePinnedStatus from '../../reducers/ChangePinnedStatus';
7 | import { Text } from "grommet";
8 | import { Pin, CircleInformation, Halt } from 'grommet-icons';
9 |
10 | const SEmptyState = styled.div`
11 | margin: auto;
12 | text-align: center;
13 |
14 | padding: 20px;
15 | `
16 |
17 | const TableTitle = styled.p`
18 | text-align: center;
19 | font-size: 90%;
20 | padding: 5px;
21 | overflow-wrap: break-word;
22 | :hover {
23 | transform: scale(1.01);
24 | }
25 | `;
26 |
27 |
28 | const STabWrapper = styled.span`
29 | font-size: 14px;
30 | transition: all 0.2s;
31 | :hover {
32 | transform: scale(1.1);
33 | }
34 | `
35 |
36 | const SIndTablButtons = styled.div`
37 | width: 100%;
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | border-bottom: 1px solid grey;
42 | padding-top: 3px;
43 | `
44 |
45 | const TempWrapper = styled.div`
46 | overflow: scroll;
47 | height: 100%;
48 | width: 100%;
49 | display: flex;
50 | flex-wrap: wrap;
51 | padding: 5px 15px;
52 | `
53 |
54 | interface ITableWrapperProps {
55 | highlightForRelationship: string;
56 | }
57 |
58 | const TableWrapper = styled.div`
59 | width: 150px;
60 | max-height: 200px;
61 | border-radius: 3px;
62 | overflow: hidden;
63 | margin: 8px;
64 | border: ${({ highlightForRelationship }) => (highlightForRelationship == 'true' ? '1px solid transparent' : '1px solid grey')};
65 | box-shadow: ${({ highlightForRelationship }) => (highlightForRelationship == 'true' ? '0px 0px 8px #4B70FE' : 'none')};
66 | `;
67 |
68 | interface IForeignKey {
69 | table: string;
70 | column: string;
71 | }
72 |
73 | let isPrimaryKey: string;
74 | let isForeignKey: string;
75 | let primaryKeyTableForForeignKey: string;
76 | let primaryKeyColumn: string;
77 | let selectedTableName: string;
78 | let selectedColumnName: string;
79 |
80 | interface ITablesContainerProps {
81 | data: any;
82 | userInputForTables: string;
83 | activeTableInPanel: any;
84 | selectedForQueryTables: any;
85 | relationships: any;
86 | captureSelectedTable: (any) => any;
87 | captureQuerySelections: (any) => any;
88 | }
89 |
90 | const TablesContainer: React.SFC = ({
91 | userInputForTables,
92 | activeTableInPanel,
93 | selectedForQueryTables,
94 | data,
95 | captureSelectedTable,
96 | captureQuerySelections,
97 | relationships
98 | }) => {
99 |
100 | const [mouseOver, setMouseOver] = useState(); //data to detect if mouse is over a pk or fk
101 | const [filteredTables, setFilteredTables] = useState([]);
102 | const [pinnedTables, setPinnedTables] = useState([]);
103 | const [pinnedTableNames, dispatchPinned] = useReducer(changePinnedStatus, []);
104 | const [foreignKeysAffected, setForeignKeysAffected] = useState([]);
105 | const [primaryKeyAffected, setPrimaryKeyAffected] = useState([
106 | {
107 | primaryKeyTable: '',
108 | primaryKeyColumn: ''
109 | }
110 | ]);
111 | const [tablesRelated, setTablesRelated] = useState([]);
112 |
113 | useEffect(() => {
114 | const temptables = [];
115 | for (let table in relationships) {
116 | temptables.push(table);
117 | for (let i = 0; i < relationships[table].length; i++) {
118 | temptables.push(relationships[table][i].fktablename)
119 | }
120 | setTablesRelated(temptables);
121 | }
122 | setTablesRelated(temptables);
123 | }, [captureQuerySelections])
124 |
125 | useEffect(() => {
126 | if (!mouseOver) {
127 | //Resets all relationships
128 | setPrimaryKeyAffected([{ primaryKeyTable: '', primaryKeyColumn: '' }]);
129 | setForeignKeysAffected([]);
130 | }
131 | //Determines which rows should be highlighted
132 | if (mouseOver) {
133 | if (isForeignKey == 'true') {
134 | setPrimaryKeyAffected([
135 | {
136 | primaryKeyTable: primaryKeyTableForForeignKey,
137 | primaryKeyColumn: primaryKeyColumn
138 | }
139 | ]);
140 | }
141 |
142 | if (isPrimaryKey === 'true') {
143 | const allForeignKeys: IForeignKey[] = [];
144 | data.forEach((table): void => {
145 | table.foreignKeys.forEach((foreignkey): void => {
146 | if (
147 | foreignkey.foreign_table_name === selectedTableName &&
148 | foreignkey.foreign_column_name === selectedColumnName
149 | )
150 | allForeignKeys.push({
151 | table: foreignkey.table_name,
152 | column: foreignkey.column_name
153 | });
154 | });
155 | });
156 | setForeignKeysAffected(allForeignKeys);
157 | }
158 | }
159 | }, [data, mouseOver, selectedForQueryTables]);
160 |
161 | //Builds out tables to display
162 | useEffect((): void => {
163 | const pinned = [];
164 | const filtered = [];
165 |
166 | if (data.length > 0) {
167 | const regex = new RegExp(userInputForTables);
168 | data.forEach(table => {
169 | let highlightForRelationship = 'false';
170 | if (tablesRelated.includes(table.table_name)) {
171 | highlightForRelationship = 'true';
172 | }
173 | if (pinnedTableNames.includes(table.table_name)) {
174 | pinned.push(
175 |
176 | {table.table_name}
177 |
178 |
179 |
183 | dispatchPinned(actions.removeFromPinned(table.table_name))
184 | }
185 | pinned='true'
186 | color="#FF98BB"
187 | />
188 |
189 |
190 |
196 |
197 |
198 | {
209 | isPrimaryKey = e.target.dataset.isprimarykey;
210 | isForeignKey = e.target.dataset.isforeignkey;
211 | primaryKeyTableForForeignKey =
212 | e.target.dataset.foreignkeytable;
213 | primaryKeyColumn = e.target.dataset.foreignkeycolumn;
214 | selectedTableName = e.target.dataset.tablename;
215 | selectedColumnName = e.target.dataset.columnname;
216 | setMouseOver(true);
217 | }}
218 | captureMouseExit={() => {
219 | setMouseOver(false);
220 | }}
221 | key={table.table_name}
222 | />
223 |
224 | );
225 | } else if (regex.test(table.table_name)) {
226 | filtered.push(
227 |
228 | {table.table_name}
229 |
230 |
231 |
232 |
236 | dispatchPinned(actions.addToPinned(table.table_name))
237 | }
238 | pinned='false'
239 | color="#485360"
240 | />
241 |
242 |
243 |
249 |
250 |
251 | {
262 | isPrimaryKey = e.target.dataset.isprimarykey;
263 | isForeignKey = e.target.dataset.isforeignkey;
264 | primaryKeyTableForForeignKey =
265 | e.target.dataset.foreignkeytable;
266 | primaryKeyColumn = e.target.dataset.foreignkeycolumn;
267 | selectedTableName = e.target.dataset.tablename;
268 | selectedColumnName = e.target.dataset.columnname;
269 | setMouseOver(true);
270 | }}
271 | captureMouseExit={() => {
272 | setMouseOver(false);
273 | }}
274 | key={table.table_name}
275 | />
276 |
277 | );
278 | }
279 | });
280 | setFilteredTables(filtered);
281 | setPinnedTables(pinned);
282 | }
283 | }, [
284 | data,
285 | foreignKeysAffected,
286 | primaryKeyAffected,
287 | userInputForTables,
288 | pinnedTableNames,
289 | activeTableInPanel,
290 | selectedForQueryTables
291 | ]);
292 |
293 | if (pinnedTables.length || filteredTables.length) {
294 | return (
295 |
296 | {pinnedTables}
297 | {filteredTables}
298 |
299 | )
300 | }
301 | return (
302 |
303 |
304 |
305 | Sorry, there are no tables that matched your search.
Please search again.
306 |
307 |
308 | )
309 |
310 | }
311 |
312 | export default TablesContainer;
--------------------------------------------------------------------------------
/app/containers/omnibox/OmniBoxContainer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 | import OmniBoxInput from '../../components/omnibox/OmniBoxInput';
4 |
5 | const OmniBoxWrapper = styled.div`
6 | box-shadow: 1px 1px 4px #67809f;
7 | background-color: white;
8 | padding: 10px;
9 | border-radius: 3px;
10 | `
11 |
12 | const QueryResultError = styled.div`
13 | font-family: 'Poppins';
14 | color: #ca333e;
15 | border-radius: 3px;
16 | border-left: 3px solid #ca333e;
17 | font-size: 80%;
18 | background-color: #f1c7ca;
19 | margin: 5px 0px;
20 | padding: 5px;
21 | `;
22 | interface IOmniBoxNavButtonProps {
23 | omniBoxView: string;
24 | selectedView: string;
25 | }
26 |
27 | const OmniBoxNavButton = styled.button`
28 | padding: 5px;
29 | width: 50%;
30 | font-size: 80%;
31 | font-family: 'Poppins', sans-serif;
32 | border-radius: 3px 3px 0px 0px;
33 | border: none;
34 | cursor: pointer;
35 | color: ${props =>
36 | props.selectedView === props.omniBoxView
37 | ? '#4B70FE'
38 | : 'grey'};
39 | font-weight: ${(props) =>
40 | props.selectedView === props.omniBoxView ? 'bold' : 'none'};
41 | :hover{
42 | font-weight: bold;
43 | }
44 | :focus {
45 | outline: none;
46 | }
47 | `;
48 |
49 | interface IOmniBoxProps {
50 | pgClient: any,
51 | userInputQuery: string;
52 | loadingQueryStatus: boolean;
53 | queryResultError: any;
54 | userInputForTables: string;
55 | omniBoxView: string;
56 | setQueryResult: (any) => any;
57 | setOmniBoxView: (any) => any;
58 | setQueryResultError: (any) => any;
59 | setLoadingQueryStatus: (any) => any;
60 | setUserInputQuery: (any) => any;
61 | setUserInputForTables: (any) => any;
62 | setActiveDisplayInResultsTab: (any) => any;
63 | }
64 |
65 | const OmniBoxContainer: React.SFC = ({
66 | pgClient,
67 | setQueryResult,
68 | userInputQuery,
69 | loadingQueryStatus,
70 | setQueryResultError,
71 | setLoadingQueryStatus,
72 | setUserInputQuery,
73 | queryResultError,
74 | setUserInputForTables,
75 | userInputForTables,
76 | omniBoxView,
77 | setOmniBoxView,
78 | setActiveDisplayInResultsTab
79 | }) => {
80 |
81 | const listOfTabNames = ['SQL', 'Search'];
82 | const navigationTabs = listOfTabNames.map(tabname => {
83 | return (
84 | {
87 | setOmniBoxView(tabname);
88 | if (tabname === 'Search') setActiveDisplayInResultsTab('Tables');
89 | }}
90 | omniBoxView={omniBoxView}
91 | selectedView={tabname}
92 | >
93 | {tabname}
94 |
95 | );
96 | });
97 |
98 | const executeQuery = (): void => {
99 | if (!loadingQueryStatus) {
100 | let query = userInputQuery;
101 | if (query.slice(0, 6).toUpperCase() === 'SELECT') {
102 | if (query.indexOf(';') > -1) query = query.slice(0, query.indexOf(';'));
103 | query += ';';
104 |
105 | setQueryResultError({
106 | status: false,
107 | message: ''
108 | });
109 | pgClient.query(query, (err, result) => {
110 | if (err) {
111 | setQueryResult({
112 | statusCode: 'Syntax Error',
113 | message: 'Issue getting data from db',
114 | err
115 | })
116 | } else {
117 | setQueryResult({
118 | statusCode: 'Success',
119 | message: result.rows
120 | })
121 | }
122 | })
123 |
124 | } else {
125 | setQueryResult({
126 | statusCode: 'Invalid Request',
127 | message: 'Invalid query input. The query can only be a SELECT statement.'
128 | });
129 |
130 | }
131 | }
132 | setLoadingQueryStatus(true);
133 | };
134 | const generateInputBox = () => {
135 | return (
136 |
146 | );
147 | };
148 |
149 |
150 | return (
151 |
152 |
153 | {generateInputBox()}
154 | {queryResultError.status && (
155 | {queryResultError.message}
156 | )}
157 |
158 | );
159 | };
160 |
161 | export default OmniBoxContainer;
--------------------------------------------------------------------------------
/app/contexts/themeContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const ThemeContext = createContext([]);
4 |
5 | export default ThemeContext;
6 |
--------------------------------------------------------------------------------
/app/db.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/app/db.js
--------------------------------------------------------------------------------
/app/db.ts:
--------------------------------------------------------------------------------
1 | const getTables = client => {
2 | return new Promise((resolve, reject) => {
3 | client.query(
4 | `SELECT table_name
5 | FROM information_schema.tables
6 | WHERE table_schema='public'
7 | AND table_type='BASE TABLE'
8 | ORDER BY table_name ASC`,
9 | (err: string, result: string) => {
10 | if (err) reject(err);
11 | resolve(result);
12 | }
13 | );
14 | });
15 | };
16 |
17 | const getForeignKeys = (client, tableName: string) => {
18 | return new Promise((resolve, reject) => {
19 | client.query(
20 | `SELECT tc.table_schema,
21 | tc.constraint_name,
22 | tc.table_name,
23 | kcu.column_name,
24 | ccu.table_schema AS foreign_table_schema,
25 | ccu.table_name AS foreign_table_name,
26 | ccu.column_name AS foreign_column_name
27 | FROM information_schema.table_constraints AS tc
28 | JOIN information_schema.key_column_usage AS kcu
29 | ON tc.constraint_name = kcu.constraint_name
30 | AND tc.table_schema = kcu.table_schema
31 | JOIN information_schema.constraint_column_usage AS ccu
32 | ON ccu.constraint_name = tc.constraint_name
33 | AND ccu.table_schema = tc.table_schema
34 | WHERE tc.constraint_type = 'FOREIGN KEY'
35 | AND tc.table_name = '${tableName}'`,
36 | (err: string, result: any) => {
37 | if (err) reject(err);
38 | resolve(result.rows);
39 | }
40 | );
41 | });
42 | };
43 |
44 | // #TODO: add error handling when tables lack a primary key
45 | // Relational database theory dictates that every table must have a primary key.
46 | // This rule is not enforced by PostgreSQL, but it is usually best to follow it.
47 | const getColumns = (client, tableName: string) => {
48 | return new Promise((resolve, reject) => {
49 | client.query(
50 | `SELECT COLUMN_NAME AS ColumnName,
51 | DATA_TYPE AS DataType,
52 | CHARACTER_MAXIMUM_LENGTH AS CharacterLength,
53 | COLUMN_DEFAULT AS DefaultValue
54 | FROM INFORMATION_SCHEMA.COLUMNS
55 | WHERE TABLE_NAME = '${tableName}'`,
56 | (err: string, result: any) => {
57 | if (err)
58 | // #TODO: give a msg that doesn't expose structure of database
59 | reject(err);
60 | resolve(result.rows);
61 | }
62 | );
63 | });
64 | };
65 |
66 | const getPrimaryKey = (client, tableName: string) => {
67 | return new Promise((resolve, reject) => {
68 | client.query(
69 | `SELECT column_name
70 | FROM pg_constraint, information_schema.constraint_column_usage
71 | WHERE contype = 'p'
72 | AND information_schema.constraint_column_usage.table_name = '${tableName}'
73 | AND pg_constraint.conname = information_schema.constraint_column_usage.constraint_name`,
74 | (err: string, result: any) => {
75 | if (err) reject(err);
76 | resolve(result.rows[0].column_name);
77 | }
78 | );
79 | });
80 | };
81 |
82 | async function composeTableData(client): Promise {
83 | const tablesArr = [];
84 | const tableNames: any = await getTables(client);
85 |
86 | for (const table of tableNames.rows) {
87 | table.primaryKey = await getPrimaryKey(client, table.table_name);
88 | table.foreignKeys = await getForeignKeys(client, table.table_name);
89 | table.columns = await getColumns(client, table.table_name);
90 | tablesArr.push(table);
91 | }
92 |
93 | return new Promise((resolve: any, reject: any): any => {
94 | if (tablesArr.length > 0) {
95 | resolve(tablesArr);
96 | } else {
97 | // #TODO: add empty state trigger
98 | reject(new Error('database empty'));
99 | }
100 | });
101 | }
102 |
103 | export default composeTableData;
104 |
--------------------------------------------------------------------------------
/app/dbProcess.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/dbProcess.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/app/dbProcess.js
--------------------------------------------------------------------------------
/app/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from 'react-dom';
3 | // import { hot } from 'react-hot-loader/root';
4 | import './app.global.css';
5 | import { ThemeProvider } from 'styled-components';
6 | import { useState, useEffect } from 'react';
7 | import ThemeContext from './contexts/themeContext';
8 | import themes from './themes/themes';
9 | import LoginPage from './containers/LoginPage';
10 | import HomePage from './containers/HomePage';
11 |
12 | const Index = () => {
13 | const modes = [
14 | { value: 'defaultTheme', active: true },
15 | { value: 'darkTheme', active: false },
16 | ]
17 | const [context, setContext] = useState(modes);
18 | const serveMode = context.reduce((acc, mode) => {
19 | if (mode.active) acc = mode.value;
20 | return acc;
21 | }, 'defaultTheme');
22 |
23 | const [pgClient, setPgClient] = useState()
24 | const [tableData, setTableData] = useState([]);
25 | const [currentView, setCurrentView] = useState('loginPage')
26 | //validate in another way error or no tables in db
27 | useEffect(() => {
28 | if (tableData.length > 0) setCurrentView('homePage')
29 | }, [])
30 |
31 | return (
32 | //
33 | //
34 |
35 | {currentView === 'loginPage' &&
36 |
41 | }
42 | {currentView === 'homePage' &&
43 |
48 | }
49 |
50 | //
51 | //
52 | );
53 | };
54 |
55 | // hot(Index);
56 |
57 | render(, document.getElementById('root'));
58 |
--------------------------------------------------------------------------------
/app/main.dev.babel.js:
--------------------------------------------------------------------------------
1 | const register = require('@babel/register').default;
2 |
3 | register({
4 | extensions: ['.ts', '.tsx', '.js', '.json']
5 | });
6 |
7 | require('./main.dev.ts');
8 |
--------------------------------------------------------------------------------
/app/main.dev.ts:
--------------------------------------------------------------------------------
1 | import { app, BrowserWindow, ipcMain } from 'electron';
2 | import MenuBuilder from './menu';
3 | import AppDb from './appDb';
4 |
5 | // import { remote } from 'electron';
6 |
7 | // import { autoUpdater } from 'electron-updater';
8 | // #TODO: make update flow (this class is currently has 0 references)
9 | // export default class AppUpdater {
10 | // public constructor() {
11 | // log.transports.file.level = 'info';
12 | // autoUpdater.logger = log;
13 | // autoUpdater.checkForUpdatesAndNotify();
14 | // }
15 | // }
16 |
17 | let mainWindow = null;
18 | // let dbProcess = null;
19 |
20 | const defaults = {
21 | configName: 'user-data',
22 | width: 800, // will be the default window size if they haven't resized ever
23 | height: 600,
24 | savedConnections: [],
25 | theme: 'default'
26 | };
27 | const appDb = new AppDb(defaults);
28 |
29 | if (process.env.NODE_ENV === 'production') {
30 | const sourceMapSupport = require('source-map-support');
31 | sourceMapSupport.install();
32 | }
33 |
34 | if (
35 | process.env.NODE_ENV === 'development' ||
36 | process.env.DEBUG_PROD === 'true'
37 | ) {
38 | process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
39 | require('electron-debug')();
40 | }
41 |
42 | const installExtensions = async () => {
43 | const installer = require('electron-devtools-installer');
44 | const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
45 |
46 | const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
47 |
48 | return Promise.all(
49 | extensions.map(name => installer.default(installer[name], forceDownload))
50 | ).catch(console.log);
51 | };
52 |
53 | app.setAboutPanelOptions({
54 | applicationName: 'SeeQL',
55 | applicationVersion: '1.0.0-beta',
56 | version: '1.0.0-beta'
57 | });
58 |
59 | app.on('ready', async () => {
60 | if (
61 | process.env.NODE_ENV === 'development' ||
62 | process.env.DEBUG_PROD === 'true'
63 | ) {
64 | await installExtensions();
65 | }
66 |
67 | mainWindow = new BrowserWindow({
68 | show: true,
69 | width: 1200,
70 | height: 800,
71 | minWidth: 600,
72 | minHeight: 500,
73 | titleBarStyle: 'hiddenInset',
74 | center: true,
75 | icon: __dirname + '../resources/seeql.png.icns',
76 | webPreferences: {
77 | nodeIntegration: true // #TODO: see if necessary
78 | }
79 | });
80 |
81 | // debugging packaged app
82 | // mainWindow.openDevTools();
83 |
84 | mainWindow.loadURL(`file://${__dirname}/app.html`);
85 | mainWindow.webContents.on('ready-to-show', () => {
86 | if (!mainWindow) throw new Error('"mainWindow" is not defined');
87 | if (process.env.START_MINIMIZED) {
88 | mainWindow.minimize();
89 | } else {
90 | // #TODO: decide do we show the app window
91 |
92 | // const connStrs = appDb.get('savedConnections');
93 | // mainWindow.webContents.send('saved-connections', connStrs);
94 |
95 | // const userTheme = appDb.get('userThemePreference');
96 | // mainWindow.webContents.send('user-theme-preference', userTheme);
97 |
98 | mainWindow.show();
99 | mainWindow.focus();
100 | }
101 | });
102 |
103 | const menuBuilder = new MenuBuilder(mainWindow);
104 | menuBuilder.buildMenu();
105 |
106 | // dbProcess = new BrowserWindow({ show: false });
107 | // dbProcess.loadURL(`file://${__dirname}/dbProcess.html`);
108 |
109 | // sent * from * the renderer process when the user selects "remember me"
110 | // savedConnStr = { name: "name for connStr", uri: 'postgres://etc' }
111 | // ipcMain.on('remember-connection', (_event: Event, savedConnStr: any) => {
112 | // appDb.set('connStr', savedConnStr);
113 | // });
114 |
115 | // saves queries to display in side-panel @ some point
116 | // ipcMain.on('query-to-main', (_event: void, query: string) => {
117 | // // dbProcess.webContents.send('query-to-db', query);
118 | // appDb.set('userQueryHistory', query);
119 | // });
120 |
121 | // // saves users theme if they select one which isn't default
122 | // ipcMain.on('user-theme-selected', (_event: Event, theme: string) => {
123 | // appDb.set('theme', theme);
124 | // });
125 |
126 | // // Listening from homepage, to send to database
127 | // ipcMain.on('uri-to-main', (_event: void, uri: string) => {
128 | // console.log('hi im uri-to-main in the main.dev.ts');
129 | // dbProcess.webContents.send('uri-to-db', uri);
130 | // });
131 |
132 | // ipcMain.on('query-to-main', (_event: void, query: string) => {
133 | // dbProcess.webContents.send('query-to-db', query);
134 | // });
135 |
136 | // ipcMain.on('logout-to-main', (_event: void, message: string) => {
137 | // dbProcess.webContents.send('logout-to-db', message);
138 | // });
139 |
140 | // ipcMain.on('login-mounted', () => {
141 | // dbProcess.webContents.send('login-mounted');
142 | // });
143 |
144 | // // Listening from database, to send to homepage
145 | // ipcMain.on(
146 | // 'database-tables-to-main',
147 | // (_event: void, databaseTables: any[]) => {
148 | // mainWindow.webContents.send('tabledata-to-login', databaseTables); // we could save/cache this data but for now let's not
149 | // console.log('hello im databasetables PLZ FUCKING WOKR', databaseTables);
150 | // }
151 | // );
152 |
153 | // ipcMain.on('db-connection-error', (_event: void, err: Error) => {
154 | // mainWindow.webContents.send('db-connection-error', err);
155 | // appDb.set('connectionError', err); // could be used in our crash reporting tools
156 | // });
157 |
158 | // ipcMain.on('query-result-to-main', (_event: void, messagePayload: any) => {
159 | // mainWindow.webContents.send('query-result-to-homepage', messagePayload);
160 | // });
161 |
162 | // ipcMain.on('inactivity-logout', (_event: void, _message: string) => {
163 | // mainWindow.webContents.send('inactivity-logout');
164 | // });
165 |
166 | // ipcMain.on('logout-reason', (_event: void, message: string) => {
167 | // mainWindow.webContents.send('logout-reason', message);
168 | // appDb.set('logoutReason', message);
169 | // });
170 |
171 | mainWindow.on('resize', () => {
172 | const { width, height } = mainWindow.getBounds();
173 | appDb.set('windowBounds', { width, height }); // getBounds returns an object with the height, width, x and y
174 | });
175 |
176 | mainWindow.on('closed', () => {
177 | mainWindow = null;
178 | });
179 |
180 | console.log(`\n\n
181 | ('-').-> ('-') <-.('-')
182 | ( OO)_ ( OO).-> __( OO) <-.
183 | (_)--\_) (,------. ,------. '-'---\_) ,--. )
184 | / _ / | .---' | .---' | .-. | | ('-')
185 | \_..'--. (| '--. | '--. | | | | | |OO )
186 | .-._) \ | .--' | .--' | | | | (| '__ |
187 | \ / | '---. | '---. ' '-' '-. | |'
188 | '-----' '------' '------' '-----'--' '-----'
189 |
190 | Have a question?
191 | Contact us on github.com/oslabs-beta/seeql
192 | \n\n
193 | `);
194 | });
195 |
196 | app.on('window-all-closed', () => {
197 | // Respect the OSX convention of having the application in memory even
198 | // after all windows have been closed
199 | if (process.platform !== 'darwin') {
200 | app.quit();
201 | }
202 | });
203 |
--------------------------------------------------------------------------------
/app/menu.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/explicit-member-accessibility */
2 | import {
3 | app,
4 | Menu,
5 | shell,
6 | BrowserWindow,
7 | MenuItemConstructorOptions
8 | } from 'electron';
9 |
10 | export default class MenuBuilder {
11 | mainWindow: BrowserWindow;
12 |
13 | constructor(mainWindow: BrowserWindow) {
14 | this.mainWindow = mainWindow;
15 | }
16 |
17 | buildMenu() {
18 | if (
19 | process.env.NODE_ENV === 'development' ||
20 | process.env.DEBUG_PROD === 'true'
21 | ) {
22 | this.setupDevelopmentEnvironment();
23 | }
24 |
25 | const template =
26 | process.platform === 'darwin'
27 | ? this.buildDarwinTemplate()
28 | : this.buildDefaultTemplate();
29 |
30 | const menu = Menu.buildFromTemplate(template);
31 | Menu.setApplicationMenu(menu);
32 |
33 | return menu;
34 | }
35 |
36 | setupDevelopmentEnvironment() {
37 | this.mainWindow.webContents.toggleDevTools();
38 | this.mainWindow.webContents.on('context-menu', (e, props) => {
39 | const { x, y } = props;
40 |
41 | Menu.buildFromTemplate([
42 | {
43 | label: 'Inspect element',
44 | click: () => {
45 | this.mainWindow.webContents.inspectElement(x, y);
46 | }
47 | }
48 | ]).popup({ window: this.mainWindow });
49 | });
50 | }
51 |
52 | buildDarwinTemplate(): MenuItemConstructorOptions[] {
53 | const subMenuAbout = {
54 | label: 'SeeQL',
55 | submenu: [
56 | {
57 | label: 'About SeeQL',
58 | selector: 'orderFrontStandardAboutPanel:'
59 | },
60 | { type: 'separator' },
61 | // { label: 'Services', submenu: [] },
62 | // { type: 'separator' },
63 | {
64 | label: 'Hide SeeQL',
65 | accelerator: 'Command+H',
66 | selector: 'hide:'
67 | },
68 | {
69 | label: 'Hide Others',
70 | accelerator: 'Command+Shift+H',
71 | selector: 'hideOtherApplications:'
72 | },
73 | { label: 'Show All', selector: 'unhideAllApplications:' },
74 | { type: 'separator' },
75 | {
76 | label: 'Quit',
77 | accelerator: 'Command+Q',
78 | click: () => {
79 | app.quit();
80 | }
81 | }
82 | ]
83 | };
84 | const subMenuEdit = {
85 | label: 'Edit',
86 | submenu: [
87 | { label: 'Undo', accelerator: 'Command+Z', selector: 'undo:' },
88 | { label: 'Redo', accelerator: 'Shift+Command+Z', selector: 'redo:' },
89 | { type: 'separator' },
90 | { label: 'Cut', accelerator: 'Command+X', selector: 'cut:' },
91 | { label: 'Copy', accelerator: 'Command+C', selector: 'copy:' },
92 | { label: 'Paste', accelerator: 'Command+V', selector: 'paste:' },
93 | {
94 | label: 'Select All',
95 | accelerator: 'Command+A',
96 | selector: 'selectAll:'
97 | }
98 | ]
99 | };
100 | const subMenuViewDev = {
101 | label: 'View',
102 | submenu: [
103 | {
104 | role: 'zoomin',
105 | accelerator: 'Command+='
106 | },
107 | {
108 | role: 'zoomout',
109 | accelerator: 'Command+-'
110 | },
111 | {
112 | label: 'Reload',
113 | accelerator: 'Command+R',
114 | click: () => {
115 | this.mainWindow.webContents.reload();
116 | }
117 | },
118 | {
119 | label: 'Toggle Full Screen',
120 | accelerator: 'Ctrl+Command+F',
121 | click: () => {
122 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
123 | }
124 | },
125 | {
126 | label: 'Toggle Developer Tools',
127 | accelerator: 'Alt+Command+I',
128 | click: () => {
129 | this.mainWindow.webContents.toggleDevTools();
130 | }
131 | }
132 | ]
133 | };
134 | const subMenuViewProd = {
135 | label: 'View',
136 | submenu: [
137 | {
138 | label: 'Toggle Full Screen',
139 | accelerator: 'Ctrl+Command+F',
140 | click: () => {
141 | this.mainWindow.setFullScreen(!this.mainWindow.isFullScreen());
142 | }
143 | }
144 | ]
145 | };
146 | const subMenuWindow = {
147 | label: 'Window',
148 | submenu: [
149 | {
150 | label: 'Minimize',
151 | accelerator: 'Command+M',
152 | selector: 'performMiniaturize:'
153 | },
154 | { label: 'Close', accelerator: 'Command+W', selector: 'performClose:' },
155 | { type: 'separator' },
156 | { label: 'Bring All to Front', selector: 'arrangeInFront:' }
157 | ]
158 | };
159 | const subMenuHelp = {
160 | label: 'Help',
161 | submenu: [
162 | {
163 | label: 'Learn More',
164 | click() {
165 | shell.openExternal('http://electron.atom.io');
166 | }
167 | },
168 | {
169 | label: 'Documentation',
170 | click() {
171 | shell.openExternal(
172 | 'https://github.com/atom/electron/tree/master/docs#readme'
173 | );
174 | }
175 | },
176 | {
177 | label: 'Community Discussions',
178 | click() {
179 | shell.openExternal('https://discuss.atom.io/c/electron');
180 | }
181 | },
182 | {
183 | label: 'Search Issues',
184 | click() {
185 | shell.openExternal('https://github.com/atom/electron/issues');
186 | }
187 | }
188 | ]
189 | };
190 |
191 | const subMenuView =
192 | process.env.NODE_ENV === 'development' ? subMenuViewDev : subMenuViewProd;
193 |
194 | return [
195 | subMenuAbout,
196 | subMenuEdit,
197 | subMenuView,
198 | subMenuWindow,
199 | subMenuHelp
200 | ] as MenuItemConstructorOptions[];
201 | }
202 |
203 | buildDefaultTemplate(): MenuItemConstructorOptions[] {
204 | const templateDefault = [
205 | {
206 | label: '&File',
207 | submenu: [
208 | { label: '&Open', accelerator: 'Ctrl+O' },
209 | {
210 | label: '&Close',
211 | accelerator: 'Ctrl+W',
212 | click: () => {
213 | this.mainWindow.close();
214 | }
215 | }
216 | ]
217 | },
218 | {
219 | label: '&View',
220 | submenu:
221 | process.env.NODE_ENV === 'development'
222 | ? [
223 | {
224 | label: '&Reload',
225 | accelerator: 'Ctrl+R',
226 | click: () => {
227 | this.mainWindow.webContents.reload();
228 | }
229 | },
230 | {
231 | label: 'Toggle &Full Screen',
232 | accelerator: 'F11',
233 | click: () => {
234 | this.mainWindow.setFullScreen(
235 | !this.mainWindow.isFullScreen()
236 | );
237 | }
238 | },
239 | {
240 | label: 'Toggle &Developer Tools',
241 | accelerator: 'Alt+Ctrl+I',
242 | click: () => {
243 | this.mainWindow.webContents.toggleDevTools();
244 | }
245 | }
246 | ]
247 | : [
248 | {
249 | label: 'Toggle &Full Screen',
250 | accelerator: 'F11',
251 | click: () => {
252 | this.mainWindow.setFullScreen(
253 | !this.mainWindow.isFullScreen()
254 | );
255 | }
256 | }
257 | ]
258 | },
259 | {
260 | label: 'Help',
261 | submenu: [
262 | {
263 | label: 'Learn More',
264 | click() {
265 | shell.openExternal('http://electron.atom.io');
266 | }
267 | },
268 | {
269 | label: 'Documentation',
270 | click() {
271 | shell.openExternal(
272 | 'https://github.com/atom/electron/tree/master/docs#readme'
273 | );
274 | }
275 | },
276 | {
277 | label: 'Community Discussions',
278 | click() {
279 | shell.openExternal('https://discuss.atom.io/c/electron');
280 | }
281 | },
282 | {
283 | label: 'Search Issues',
284 | click() {
285 | shell.openExternal('https://github.com/atom/electron/issues');
286 | }
287 | }
288 | ]
289 | }
290 | ];
291 |
292 | return templateDefault;
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/app/reducers/ChangeDisplayOfSidePanel.tsx:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes';
2 |
3 | export default function changedisplayOfSidePanel(state, action) {
4 | switch (action.type) {
5 | case types.CHANGE_TO_INFO_PANEL:
6 | return 'info';
7 | case types.CHANGE_TO_FAV_PANEL:
8 | return 'favorites';
9 | case types.CHANGE_TO_SETTINGS_PANEL:
10 | return 'settings';
11 | default:
12 | throw new Error();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/app/reducers/ChangePinnedStatus.tsx:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes';
2 |
3 | export default function changePinnedStatus(state, action) {
4 | const pinnedItems = state.slice();
5 | switch (action.type) {
6 | case types.REMOVE_FROM_PINNED:
7 | const newPinnedItems = state.filter(
8 | table => table !== action.payload.tablename
9 | );
10 | return newPinnedItems;
11 | case types.ADD_TO_PINNED:
12 | pinnedItems.push(action.payload.tablename);
13 | return pinnedItems;
14 | default:
15 | throw new Error();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/reducers/themeReducer.ts:
--------------------------------------------------------------------------------
1 |
2 | export default function themeReducer(state, action) {
3 |
4 | switch (action.type) {
5 | case 'CHANGE_MODE': {
6 | const newState = state.map(mode => {
7 | if (mode['value'] === action.selected) mode = { value: mode['value'], active: true }
8 | if (mode['value'] !== action.selected) mode = { value: mode['value'], active: false }
9 |
10 | return mode;
11 | });
12 | return newState;
13 | }
14 | default: {
15 | return state;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/themes/darkTheme.ts:
--------------------------------------------------------------------------------
1 | const darkTheme = {
2 | panel: { baseColor: 'black', fontColor: 'white', headerColor: 'orange' },
3 | tabs: { baseColor: 'grey', baseColorActive: 'yellow', fontColor: 'brown' },
4 | omniBox: {
5 | buttonColor: 'blue',
6 | buttonColorActive: 'white',
7 | fontColor: 'red',
8 | fontColorActive: 'orange'
9 | },
10 | main: { baseColor: 'black' },
11 | executeButton: {
12 | baseColor: 'black',
13 | border: '1px solid white',
14 | fontColor: 'white'
15 | },
16 | tables: {
17 | highlight: 'lightpink',
18 | row: 'orange',
19 | navButtonSelect: 'orange',
20 | navButtonBase: 'transparent',
21 | navButtonFontColor: 'red',
22 | navButtonHover: 'purple',
23 | resetButton: 'pink',
24 | infoButton: 'orange',
25 | pinnedButton: 'red',
26 | pinnedButtonFontColor: 'blue',
27 | pinnedHover: 'pink'
28 | },
29 | link: { signOut: 'yellow' }
30 | }
31 |
32 | export default darkTheme;
--------------------------------------------------------------------------------
/app/themes/defaultTheme.ts:
--------------------------------------------------------------------------------
1 | const themes = {
2 | defaultTheme: {
3 | mainPanelBackground: '#E6EAF2',
4 | headerBackground: '#F7F9FD',
5 | fontColor: '#485360',
6 |
7 | },
8 | darkTheme: {
9 | mainPanelBackground: '#E6EAF2',
10 | headerBackground: '#F7F9FD',
11 | fontColor: '#485360',
12 | },
13 | gradientTheme: {
14 |
15 | },
16 | gloTheme: {
17 |
18 | }
19 | }
20 | export default themes;
--------------------------------------------------------------------------------
/app/themes/happiTheme.ts:
--------------------------------------------------------------------------------
1 | const happi= {
2 | panel: { baseColor: 'red', fontColor: 'blue', headerColor: 'orange' },
3 | tabs: { baseColor: 'yellow', baseColorActive: 'black', fontColor: 'white' },
4 | omniBox: {
5 | buttonColor: 'lightblue',
6 | buttonColorActive: 'aquamarine',
7 | fontColor: 'darkgreen',
8 | fontColorActive: 'orange'
9 | },
10 | main: { baseColor: 'pink' },
11 | executeButton: {
12 | baseColor: 'blue',
13 | border: 'none',
14 | fontColor: 'darkgreen'
15 | },
16 | tables: {
17 | highlight: 'orange',
18 | row: 'lightblue',
19 | navButtonSelect: 'orange',
20 | navButtonBase: 'transparent',
21 | navButtonFontColor: 'white',
22 | navButtonHover: 'blue',
23 | resetButton: 'orange',
24 | infoButton: 'orange',
25 | pinnedButton: 'red',
26 | pinnedButtonFontColor: 'blue',
27 | pinnedHover: 'pink'
28 | },
29 | link: { signOut: 'purple' }
30 | }
31 |
32 | export default happi;
--------------------------------------------------------------------------------
/app/themes/kateTheme.ts:
--------------------------------------------------------------------------------
1 | const kateTheme = {
2 | panel: { baseColor: 'green', fontColor: 'red', headerColor: 'orange' },
3 | tabs: { baseColor: 'orange', baseColorActive: 'black', fontColor: 'red' },
4 | omniBox: {
5 | buttonColor: 'lightgreen',
6 | buttonColorActive: 'brown',
7 | fontColor: 'pink',
8 | fontColorActive: 'magenta'
9 | },
10 | main: { baseColor: 'green' },
11 | executeButton: {
12 | baseColor: 'lightgreen',
13 | border: '1px solid yellow',
14 | fontColor: 'green'
15 | },
16 | tables: {
17 | highlight: 'lightgreen',
18 | row: 'yellow',
19 | navButtonSelect: 'orange',
20 | navButtonBase: 'transparent',
21 | navButtonFontColor: 'blue',
22 | navButtonHover: 'green',
23 | resetButton: 'green',
24 | infoButton: 'orange',
25 | pinnedButton: 'red',
26 | pinnedButtonFontColor: 'blue',
27 | pinnedHover: 'pink'
28 | },
29 | link: { signOut: 'orange' }
30 | }
31 |
32 | export default kateTheme;
--------------------------------------------------------------------------------
/app/themes/themes.ts:
--------------------------------------------------------------------------------
1 | import vaderette from './vaderetteTheme';
2 | import happi from './happiTheme';
3 | import kateTheme from './kateTheme';
4 | import tylerTheme from './tylerTheme'
5 | import darkTheme from './darkTheme'
6 | import defaultTheme from './defaultTheme'
7 |
8 | const themes ={
9 | defaultTheme, darkTheme, kateTheme, vaderette, tylerTheme, happi
10 | }
11 |
12 | export default themes;
13 |
--------------------------------------------------------------------------------
/app/themes/tylerTheme.ts:
--------------------------------------------------------------------------------
1 | const tylerTheme = {
2 | panel: { baseColor: 'orange', fontColor: 'black', headerColor: 'blue' },
3 | tabs: { baseColor: 'green', baseColorActive: 'red', fontColor: 'purple' },
4 | omniBox: {
5 | buttonColor: 'brown',
6 | buttonColorActive: 'black',
7 | fontColor: 'yellow',
8 | fontColorActive: 'lightgreen'
9 | },
10 | main: { baseColor: 'peach' },
11 | executeButton: {
12 | baseColor: 'white',
13 | border: '1px solid red',
14 | fontColor: 'orange'
15 | },
16 | tables: {
17 | highlight: 'yellow',
18 | row: 'lightpurple',
19 | navButtonSelect: 'orange',
20 | navButtonBase: 'transparent',
21 | navButtonFontColor: 'purple',
22 | navButtonHover: 'blue',
23 | resetButton: 'purple',
24 | infoButton: 'orange',
25 | pinnedButton: 'red',
26 | pinnedButtonFontColor: 'blue',
27 | pinnedHover: 'pink'
28 | },
29 | link: { signOut: 'pink' }
30 | }
31 | export default tylerTheme;
--------------------------------------------------------------------------------
/app/themes/vaderetteTheme.ts:
--------------------------------------------------------------------------------
1 | const vaderette= {
2 | panel: { baseColor: 'grey', fontColor: 'magenta', headerColor: 'orange' },
3 | tabs: {
4 | baseColor: 'magenta',
5 | baseColorActive: 'brown',
6 | fontColor: 'black'
7 | },
8 | omniBox: {
9 | buttonColor: 'grey',
10 | buttonColorActive: 'coral',
11 | fontColor: 'brown',
12 | fontColorActive: 'orange'
13 | },
14 | main: { baseColor: 'lavender' },
15 | executeButton: {
16 | baseColor: 'lightpink',
17 | border: '1px solid darkpurple',
18 | fontColor: 'black'
19 | },
20 | tables: {
21 | highlight: 'lightblue',
22 | row: 'lightpink',
23 | navButtonSelect: 'orange',
24 | navButtonBase: 'transparent',
25 | navButtonFontColor: 'brown',
26 | navButtonHover: 'yellow',
27 | resetButton: 'navy',
28 | infoButton: 'orange',
29 | pinnedButton: 'red',
30 | pinnedButtonFontColor: 'blue',
31 | pinnedHover: 'pink'
32 | },
33 | link: { signOut: 'green' }
34 | }
35 |
36 | export default vaderette;
--------------------------------------------------------------------------------
/app/utils/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/app/utils/.gitkeep
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | image: Visual Studio 2017
2 |
3 | platform:
4 | - x64
5 |
6 | environment:
7 | matrix:
8 | - nodejs_version: 10
9 |
10 | cache:
11 | - '%LOCALAPPDATA%/Yarn'
12 | - node_modules
13 | - flow-typed
14 | - '%USERPROFILE%\.electron'
15 |
16 | matrix:
17 | fast_finish: true
18 |
19 | build: off
20 |
21 | version: '{build}'
22 |
23 | shallow_clone: true
24 |
25 | clone_depth: 1
26 |
27 | install:
28 | - ps: Install-Product node $env:nodejs_version x64
29 | - set CI=true
30 | - yarn
31 |
32 | test_script:
33 | - yarn package-ci
34 | - yarn lint
35 | - yarn test
36 | - yarn build-e2e
37 | - yarn test-e2e
38 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /* eslint global-require: off */
3 |
4 | const developmentEnvironments = ['development', 'test'];
5 |
6 | const developmentPlugins = [require('react-hot-loader/babel')];
7 |
8 | const productionPlugins = [
9 | require('babel-plugin-dev-expression'),
10 |
11 | // babel-preset-react-optimize
12 | require('@babel/plugin-transform-react-constant-elements'),
13 | require('@babel/plugin-transform-react-inline-elements'),
14 | require('babel-plugin-transform-react-remove-prop-types'),
15 | // require('@babel/plugin-transform-runtime', {
16 | // regenerator: true,
17 | // absoluteRuntime: true
18 | // })
19 | ];
20 |
21 | module.exports = api => {
22 | // see docs about api at https://babeljs.io/docs/en/config-files#apicache
23 |
24 | const development = api.env(developmentEnvironments);
25 |
26 | return {
27 | presets: [
28 | [
29 | require('@babel/preset-env'),
30 | {
31 | targets: { electron: require('electron/package.json').version },
32 | useBuiltIns: 'usage',
33 | corejs: 3
34 | }
35 | ],
36 | require('@babel/preset-typescript'),
37 | [require('@babel/preset-react'), { development }]
38 | ],
39 | plugins: [
40 | // Stage 0
41 | require('@babel/plugin-proposal-function-bind'),
42 |
43 | // Stage 1
44 | require('@babel/plugin-proposal-export-default-from'),
45 | require('@babel/plugin-proposal-logical-assignment-operators'),
46 | [require('@babel/plugin-proposal-optional-chaining'), { loose: false }],
47 | [
48 | require('@babel/plugin-proposal-pipeline-operator'),
49 | { proposal: 'minimal' }
50 | ],
51 | [
52 | require('@babel/plugin-proposal-nullish-coalescing-operator'),
53 | { loose: false }
54 | ],
55 | require('@babel/plugin-proposal-do-expressions'),
56 |
57 | // Stage 2
58 | [require('@babel/plugin-proposal-decorators'), { legacy: true }],
59 | require('@babel/plugin-proposal-function-sent'),
60 | require('@babel/plugin-proposal-export-namespace-from'),
61 | require('@babel/plugin-proposal-numeric-separator'),
62 | require('@babel/plugin-proposal-throw-expressions'),
63 |
64 | // Stage 3
65 | require('@babel/plugin-syntax-dynamic-import'),
66 | require('@babel/plugin-syntax-import-meta'),
67 | [require('@babel/plugin-proposal-class-properties'), { loose: true }],
68 | require('@babel/plugin-proposal-json-strings'),
69 | // require('babel-plugin-transform-object-rest-spread'),
70 | require('@babel/plugin-proposal-object-rest-spread'),
71 | require('@babel/plugin-transform-runtime', { regenerator: true }),
72 |
73 | ...(development ? developmentPlugins : productionPlugins)
74 | ]
75 | };
76 | };
77 |
--------------------------------------------------------------------------------
/configs/webpack.config.base.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import webpack from 'webpack';
3 | import { dependencies } from '../package.json';
4 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
5 |
6 | export default {
7 | externals: [...Object.keys(dependencies || {})],
8 | module: {
9 | rules: [
10 | {
11 | test: /\.(j|t)sx?$/,
12 | exclude: /node_modules/,
13 | use: {
14 | loader: 'babel-loader',
15 | options: {
16 | cacheDirectory: true,
17 | babelrc: false,
18 | presets: [
19 | [
20 | '@babel/preset-env',
21 | { targets: { browsers: 'last 2 versions' } } // #TODO: electron@our.version?
22 | ],
23 | '@babel/preset-typescript',
24 | '@babel/preset-react'
25 | ],
26 | plugins: [
27 | // plugin-proposal-decorators is only needed if you're using experimental decorators in TypeScript
28 | ['@babel/plugin-proposal-decorators', { legacy: true }],
29 | ['@babel/plugin-proposal-class-properties', { loose: true }],
30 | 'react-hot-loader/babel'
31 | ]
32 | }
33 | }
34 | }
35 | ]
36 | },
37 |
38 | output: {
39 | path: path.join(__dirname, '..', 'app'),
40 | // https://github.com/webpack/webpack/issues/1114
41 | libraryTarget: 'commonjs2'
42 | },
43 |
44 | resolve: {
45 | extensions: ['.js', '.ts', '.tsx', '.json'],
46 | alias: {
47 | 'react-dom': '@hot-loader/react-dom'
48 | }
49 | },
50 |
51 | plugins: [
52 | new webpack.EnvironmentPlugin({
53 | NODE_ENV: 'development'
54 | }),
55 | new webpack.NamedModulesPlugin()
56 | ]
57 | };
58 |
--------------------------------------------------------------------------------
/configs/webpack.config.eslint.js:
--------------------------------------------------------------------------------
1 | /* eslint import/no-unresolved: off, import/no-self-import: off */
2 | require('@babel/register');
3 |
4 | module.exports = require('./webpack.config.renderer.dev.babel').default;
5 |
--------------------------------------------------------------------------------
/configs/webpack.config.main.prod.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Webpack config for production electron main process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import merge from 'webpack-merge';
8 | import TerserPlugin from 'terser-webpack-plugin';
9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
10 | import baseConfig from './webpack.config.base';
11 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
12 |
13 | CheckNodeEnv('production');
14 |
15 | export default merge.smart(baseConfig, {
16 | devtool: 'source-map',
17 |
18 | mode: 'production',
19 |
20 | target: 'electron-main',
21 |
22 | entry: './app/main.dev',
23 |
24 | output: {
25 | path: path.join(__dirname, '..'),
26 | filename: './app/main.prod.js'
27 | },
28 |
29 | optimization: {
30 | minimizer: process.env.E2E_BUILD
31 | ? []
32 | : [
33 | new TerserPlugin({
34 | parallel: true,
35 | sourceMap: true,
36 | cache: true
37 | })
38 | ]
39 | },
40 |
41 | plugins: [
42 | new BundleAnalyzerPlugin({
43 | analyzerMode:
44 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
45 | openAnalyzer: process.env.OPEN_ANALYZER === 'true'
46 | }),
47 |
48 | /**
49 | * Create global constants which can be configured at compile time.
50 | *
51 | * Useful for allowing different behaviour between development builds and
52 | * release builds
53 | *
54 | * NODE_ENV should be production so that modules do not perform certain
55 | * development checks
56 | */
57 | new webpack.EnvironmentPlugin({
58 | NODE_ENV: 'production',
59 | DEBUG_PROD: false,
60 | START_MINIMIZED: false
61 | })
62 | ],
63 |
64 | /**
65 | * Disables webpack processing of __dirname and __filename.
66 | * If you run the bundle in node.js it falls back to these values of node.js.
67 | * https://github.com/webpack/webpack/issues/2010
68 | */
69 | node: {
70 | __dirname: false,
71 | __filename: false
72 | }
73 | });
74 |
--------------------------------------------------------------------------------
/configs/webpack.config.renderer.dev.babel.js:
--------------------------------------------------------------------------------
1 | /* eslint global-require: off, import/no-dynamic-require: off */
2 |
3 | /**
4 | * Build config for development electron renderer process that uses
5 | * Hot-Module-Replacement
6 | *
7 | * https://webpack.js.org/concepts/hot-module-replacement/
8 | */
9 |
10 | import path from 'path';
11 | import fs from 'fs';
12 | import webpack from 'webpack';
13 | import chalk from 'chalk';
14 | import merge from 'webpack-merge';
15 | import { spawn, execSync } from 'child_process';
16 | import baseConfig from './webpack.config.base';
17 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
18 |
19 | CheckNodeEnv('development');
20 |
21 | const port = process.env.PORT || 1212;
22 | const publicPath = `http://localhost:${port}/dist`;
23 | const dll = path.join(__dirname, '..', 'dll');
24 | const manifest = path.resolve(dll, 'renderer.json');
25 | const requiredByDLLConfig = module.parent.filename.includes(
26 | 'webpack.config.renderer.dev.dll'
27 | );
28 |
29 | /**
30 | * Warn if the DLL is not built
31 | */
32 | if (!requiredByDLLConfig && !(fs.existsSync(dll) && fs.existsSync(manifest))) {
33 | console.log(
34 | chalk.black.bgYellow.bold(
35 | 'The DLL files are missing. Sit back while we build them for you with "yarn build-dll"'
36 | )
37 | );
38 | execSync('yarn build-dll');
39 | }
40 |
41 | export default merge.smart(baseConfig, {
42 | devtool: 'inline-source-map',
43 |
44 | mode: 'development',
45 |
46 | target: 'electron-renderer',
47 |
48 | entry: [
49 | 'react-hot-loader/babel',
50 | `webpack-dev-server/client?http://localhost:${port}/`,
51 | 'webpack/hot/only-dev-server',
52 | require.resolve('../app/index.tsx')
53 | ],
54 |
55 | output: {
56 | publicPath: `http://localhost:${port}/dist/`,
57 | filename: 'renderer.dev.js'
58 | },
59 |
60 | module: {
61 | rules: [
62 | {
63 | test: /\.global\.css$/,
64 | use: [
65 | {
66 | loader: 'style-loader'
67 | },
68 | {
69 | loader: 'css-loader',
70 | options: {
71 | sourceMap: true
72 | }
73 | }
74 | ]
75 | },
76 | {
77 | test: /^((?!\.global).)*\.css$/,
78 | use: [
79 | {
80 | loader: 'style-loader'
81 | },
82 | {
83 | loader: 'css-loader',
84 | options: {
85 | modules: true,
86 | sourceMap: true,
87 | importLoaders: 1,
88 | localIdentName: '[name]__[local]__[hash:base64:5]'
89 | }
90 | }
91 | ]
92 | },
93 | // SASS support - compile all .global.scss files and pipe it to style.css
94 | {
95 | test: /\.global\.(scss|sass)$/,
96 | use: [
97 | {
98 | loader: 'style-loader'
99 | },
100 | {
101 | loader: 'css-loader',
102 | options: {
103 | sourceMap: true
104 | }
105 | },
106 | {
107 | loader: 'sass-loader'
108 | }
109 | ]
110 | },
111 | // SASS support - compile all other .scss files and pipe it to style.css
112 | {
113 | test: /^((?!\.global).)*\.(scss|sass)$/,
114 | use: [
115 | {
116 | loader: 'style-loader'
117 | },
118 | {
119 | loader: 'css-loader',
120 | options: {
121 | modules: true,
122 | sourceMap: true,
123 | importLoaders: 1,
124 | localIdentName: '[name]__[local]__[hash:base64:5]'
125 | }
126 | },
127 | {
128 | loader: 'sass-loader'
129 | }
130 | ]
131 | },
132 | // WOFF Font
133 | {
134 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
135 | use: {
136 | loader: 'url-loader',
137 | options: {
138 | limit: 10000,
139 | mimetype: 'application/font-woff'
140 | }
141 | }
142 | },
143 | // WOFF2 Font
144 | {
145 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
146 | use: {
147 | loader: 'url-loader',
148 | options: {
149 | limit: 10000,
150 | mimetype: 'application/font-woff'
151 | }
152 | }
153 | },
154 | // TTF Font
155 | {
156 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
157 | use: {
158 | loader: 'url-loader',
159 | options: {
160 | limit: 10000,
161 | mimetype: 'application/octet-stream'
162 | }
163 | }
164 | },
165 | // EOT Font
166 | {
167 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
168 | use: 'file-loader'
169 | },
170 | // SVG Font
171 | {
172 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
173 | use: {
174 | loader: 'url-loader',
175 | options: {
176 | limit: 10000,
177 | mimetype: 'image/svg+xml'
178 | }
179 | }
180 | },
181 | // Common Image Formats
182 | {
183 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
184 | use: 'url-loader'
185 | }
186 | ]
187 | },
188 |
189 | plugins: [
190 | requiredByDLLConfig
191 | ? null
192 | : new webpack.DllReferencePlugin({
193 | context: path.join(__dirname, '..', 'dll'),
194 | manifest: require(manifest),
195 | sourceType: 'var'
196 | }),
197 |
198 | new webpack.HotModuleReplacementPlugin({
199 | multiStep: true
200 | }),
201 |
202 | new webpack.NoEmitOnErrorsPlugin(),
203 |
204 | /**
205 | * Create global constants which can be configured at compile time.
206 | *
207 | * Useful for allowing different behaviour between development builds and
208 | * release builds
209 | *
210 | * NODE_ENV should be production so that modules do not perform certain
211 | * development checks
212 | *
213 | * By default, use 'development' as NODE_ENV. This can be overriden with
214 | * 'staging', for example, by changing the ENV variables in the npm scripts
215 | */
216 | new webpack.EnvironmentPlugin({
217 | NODE_ENV: 'development'
218 | }),
219 |
220 | new webpack.LoaderOptionsPlugin({
221 | debug: true
222 | })
223 | ],
224 |
225 | node: {
226 | __dirname: false,
227 | __filename: false
228 | },
229 |
230 | devServer: {
231 | port,
232 | publicPath,
233 | compress: true,
234 | noInfo: true,
235 | stats: 'errors-only',
236 | inline: true,
237 | lazy: false,
238 | hot: true,
239 | headers: { 'Access-Control-Allow-Origin': '*' },
240 | contentBase: path.join(__dirname, 'dist'),
241 | watchOptions: {
242 | aggregateTimeout: 300,
243 | ignored: /node_modules/,
244 | poll: 100
245 | },
246 | historyApiFallback: {
247 | verbose: true,
248 | disableDotRule: false
249 | },
250 | before() {
251 | if (process.env.START_HOT) {
252 | console.log('Starting Main Process...');
253 | spawn('npm', ['run', 'start-main-dev'], {
254 | shell: true,
255 | env: process.env,
256 | stdio: 'inherit'
257 | })
258 | .on('close', code => process.exit(code))
259 | .on('error', spawnError => console.error(spawnError));
260 | }
261 | }
262 | }
263 | });
264 |
--------------------------------------------------------------------------------
/configs/webpack.config.renderer.dev.dll.babel.js:
--------------------------------------------------------------------------------
1 | /* eslint global-require: off, import/no-dynamic-require: off */
2 |
3 | /**
4 | * Builds the DLL for development electron renderer process
5 | */
6 |
7 | import webpack from 'webpack';
8 | import path from 'path';
9 | import merge from 'webpack-merge';
10 | import baseConfig from './webpack.config.base';
11 | import { dependencies } from '../package.json';
12 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
13 |
14 | CheckNodeEnv('development');
15 |
16 | const dist = path.join(__dirname, '..', 'dll');
17 |
18 | export default merge.smart(baseConfig, {
19 | context: path.join(__dirname, '..'),
20 |
21 | devtool: 'eval',
22 |
23 | mode: 'development',
24 |
25 | target: 'electron-renderer',
26 |
27 | externals: ['fsevents', 'crypto-browserify'],
28 |
29 | /**
30 | * Use `module` from `webpack.config.renderer.dev.js`
31 | */
32 | module: require('./webpack.config.renderer.dev.babel').default.module,
33 |
34 | entry: {
35 | renderer: Object.keys(dependencies || {})
36 | },
37 |
38 | output: {
39 | library: 'renderer',
40 | path: dist,
41 | filename: '[name].dev.dll.js',
42 | libraryTarget: 'var'
43 | },
44 |
45 | plugins: [
46 | new webpack.DllPlugin({
47 | path: path.join(dist, '[name].json'),
48 | name: '[name]'
49 | }),
50 |
51 | /**
52 | * Create global constants which can be configured at compile time.
53 | *
54 | * Useful for allowing different behaviour between development builds and
55 | * release builds
56 | *
57 | * NODE_ENV should be production so that modules do not perform certain
58 | * development checks
59 | */
60 | new webpack.EnvironmentPlugin({
61 | NODE_ENV: 'development'
62 | }),
63 |
64 | new webpack.LoaderOptionsPlugin({
65 | debug: true,
66 | options: {
67 | context: path.join(__dirname, '..', 'app'),
68 | output: {
69 | path: path.join(__dirname, '..', 'dll')
70 | }
71 | }
72 | })
73 | ]
74 | });
75 |
--------------------------------------------------------------------------------
/configs/webpack.config.renderer.prod.babel.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Build config for electron renderer process
3 | */
4 |
5 | import path from 'path';
6 | import webpack from 'webpack';
7 | import MiniCssExtractPlugin from 'mini-css-extract-plugin';
8 | import OptimizeCSSAssetsPlugin from 'optimize-css-assets-webpack-plugin';
9 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
10 | import merge from 'webpack-merge';
11 | import TerserPlugin from 'terser-webpack-plugin';
12 | import baseConfig from './webpack.config.base';
13 | import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
14 |
15 | CheckNodeEnv('production');
16 | export default merge.smart(baseConfig, {
17 | devtool: 'source-map',
18 |
19 | mode: 'production',
20 |
21 | target: 'electron-renderer',
22 |
23 | entry: path.join(__dirname, '..', 'app/index'),
24 |
25 | output: {
26 | path: path.join(__dirname, '..', 'app/dist'),
27 | publicPath: './dist/',
28 | filename: 'renderer.prod.js'
29 | },
30 |
31 | module: {
32 | rules: [
33 | // Extract all .global.css to style.css as is
34 | {
35 | test: /\.global\.css$/,
36 | use: [
37 | {
38 | loader: MiniCssExtractPlugin.loader,
39 | options: {
40 | publicPath: './'
41 | }
42 | },
43 | {
44 | loader: 'css-loader',
45 | options: {
46 | sourceMap: true
47 | }
48 | }
49 | ]
50 | },
51 | // Pipe other styles through css modules and append to style.css
52 | {
53 | test: /^((?!\.global).)*\.css$/,
54 | use: [
55 | {
56 | loader: MiniCssExtractPlugin.loader
57 | },
58 | {
59 | loader: 'css-loader',
60 | options: {
61 | modules: true,
62 | localIdentName: '[name]__[local]__[hash:base64:5]',
63 | sourceMap: true
64 | }
65 | }
66 | ]
67 | },
68 | // Add SASS support - compile all .global.scss files and pipe it to style.css
69 | {
70 | test: /\.global\.(scss|sass)$/,
71 | use: [
72 | {
73 | loader: MiniCssExtractPlugin.loader
74 | },
75 | {
76 | loader: 'css-loader',
77 | options: {
78 | sourceMap: true,
79 | importLoaders: 1
80 | }
81 | },
82 | {
83 | loader: 'sass-loader',
84 | options: {
85 | sourceMap: true
86 | }
87 | }
88 | ]
89 | },
90 | // Add SASS support - compile all other .scss files and pipe it to style.css
91 | {
92 | test: /^((?!\.global).)*\.(scss|sass)$/,
93 | use: [
94 | {
95 | loader: MiniCssExtractPlugin.loader
96 | },
97 | {
98 | loader: 'css-loader',
99 | options: {
100 | modules: true,
101 | importLoaders: 1,
102 | localIdentName: '[name]__[local]__[hash:base64:5]',
103 | sourceMap: true
104 | }
105 | },
106 | {
107 | loader: 'sass-loader',
108 | options: {
109 | sourceMap: true
110 | }
111 | }
112 | ]
113 | },
114 | // WOFF Font
115 | {
116 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
117 | use: {
118 | loader: 'url-loader',
119 | options: {
120 | limit: 10000,
121 | mimetype: 'application/font-woff'
122 | }
123 | }
124 | },
125 | // WOFF2 Font
126 | {
127 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
128 | use: {
129 | loader: 'url-loader',
130 | options: {
131 | limit: 10000,
132 | mimetype: 'application/font-woff'
133 | }
134 | }
135 | },
136 | // TTF Font
137 | {
138 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
139 | use: {
140 | loader: 'url-loader',
141 | options: {
142 | limit: 10000,
143 | mimetype: 'application/octet-stream'
144 | }
145 | }
146 | },
147 | // EOT Font
148 | {
149 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
150 | use: 'file-loader'
151 | },
152 | // SVG Font
153 | {
154 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
155 | use: {
156 | loader: 'url-loader',
157 | options: {
158 | limit: 10000,
159 | mimetype: 'image/svg+xml'
160 | }
161 | }
162 | },
163 | // Common Image Formats
164 | {
165 | test: /\.(?:ico|gif|png|jpg|jpeg|webp)$/,
166 | use: 'url-loader'
167 | }
168 | ]
169 | },
170 |
171 | optimization: {
172 | minimizer: process.env.E2E_BUILD
173 | ? []
174 | : [
175 | new TerserPlugin({
176 | parallel: true,
177 | sourceMap: true,
178 | cache: true
179 | }),
180 | new OptimizeCSSAssetsPlugin({
181 | cssProcessorOptions: {
182 | map: {
183 | inline: false,
184 | annotation: true
185 | }
186 | }
187 | })
188 | ]
189 | },
190 |
191 | plugins: [
192 | /**
193 | * Create global constants which can be configured at compile time.
194 | *
195 | * Useful for allowing different behaviour between development builds and
196 | * release builds
197 | *
198 | * NODE_ENV should be production so that modules do not perform certain
199 | * development checks
200 | */
201 | new webpack.EnvironmentPlugin({
202 | NODE_ENV: 'production'
203 | }),
204 |
205 | new MiniCssExtractPlugin({
206 | filename: 'style.css'
207 | }),
208 |
209 | new BundleAnalyzerPlugin({
210 | analyzerMode:
211 | process.env.OPEN_ANALYZER === 'true' ? 'server' : 'disabled',
212 | openAnalyzer: process.env.OPEN_ANALYZER === 'true'
213 | })
214 | ]
215 | });
216 |
--------------------------------------------------------------------------------
/internals/img/eslint-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/eslint-padded-90.png
--------------------------------------------------------------------------------
/internals/img/eslint-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/eslint-padded.png
--------------------------------------------------------------------------------
/internals/img/eslint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/eslint.png
--------------------------------------------------------------------------------
/internals/img/flow-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/flow-padded-90.png
--------------------------------------------------------------------------------
/internals/img/flow-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/flow-padded.png
--------------------------------------------------------------------------------
/internals/img/flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/flow.png
--------------------------------------------------------------------------------
/internals/img/jest-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/jest-padded-90.png
--------------------------------------------------------------------------------
/internals/img/jest-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/jest-padded.png
--------------------------------------------------------------------------------
/internals/img/jest.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/jest.png
--------------------------------------------------------------------------------
/internals/img/js-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/js-padded.png
--------------------------------------------------------------------------------
/internals/img/js.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/js.png
--------------------------------------------------------------------------------
/internals/img/npm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/npm.png
--------------------------------------------------------------------------------
/internals/img/react-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/react-padded-90.png
--------------------------------------------------------------------------------
/internals/img/react-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/react-padded.png
--------------------------------------------------------------------------------
/internals/img/react-router-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/react-router-padded-90.png
--------------------------------------------------------------------------------
/internals/img/react-router-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/react-router-padded.png
--------------------------------------------------------------------------------
/internals/img/react-router.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/react-router.png
--------------------------------------------------------------------------------
/internals/img/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/react.png
--------------------------------------------------------------------------------
/internals/img/redux-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/redux-padded-90.png
--------------------------------------------------------------------------------
/internals/img/redux-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/redux-padded.png
--------------------------------------------------------------------------------
/internals/img/redux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/redux.png
--------------------------------------------------------------------------------
/internals/img/webpack-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/webpack-padded-90.png
--------------------------------------------------------------------------------
/internals/img/webpack-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/webpack-padded.png
--------------------------------------------------------------------------------
/internals/img/webpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/webpack.png
--------------------------------------------------------------------------------
/internals/img/yarn-padded-90.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/yarn-padded-90.png
--------------------------------------------------------------------------------
/internals/img/yarn-padded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/yarn-padded.png
--------------------------------------------------------------------------------
/internals/img/yarn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/internals/img/yarn.png
--------------------------------------------------------------------------------
/internals/mocks/fileMock.js:
--------------------------------------------------------------------------------
1 | export default 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/internals/scripts/CheckBuiltsExist.js:
--------------------------------------------------------------------------------
1 | // Check if the renderer and main bundles are built
2 | import path from 'path';
3 | import chalk from 'chalk';
4 | import fs from 'fs';
5 |
6 | function CheckBuildsExist() {
7 | const mainPath = path.join(__dirname, '..', '..', 'app', 'main.prod.js');
8 | const rendererPath = path.join(
9 | __dirname,
10 | '..',
11 | '..',
12 | 'app',
13 | 'dist',
14 | 'renderer.prod.js'
15 | );
16 |
17 | if (!fs.existsSync(mainPath)) {
18 | throw new Error(
19 | chalk.whiteBright.bgRed.bold(
20 | 'The main process is not built yet. Build it by running "yarn build-main"'
21 | )
22 | );
23 | }
24 |
25 | if (!fs.existsSync(rendererPath)) {
26 | throw new Error(
27 | chalk.whiteBright.bgRed.bold(
28 | 'The renderer process is not built yet. Build it by running "yarn build-renderer"'
29 | )
30 | );
31 | }
32 | }
33 |
34 | CheckBuildsExist();
35 |
--------------------------------------------------------------------------------
/internals/scripts/CheckNodeEnv.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | export default function CheckNodeEnv(expectedEnv) {
4 | if (!expectedEnv) {
5 | throw new Error('"expectedEnv" not set');
6 | }
7 |
8 | if (process.env.NODE_ENV !== expectedEnv) {
9 | console.log(
10 | chalk.whiteBright.bgRed.bold(
11 | `"process.env.NODE_ENV" must be "${expectedEnv}" to use this webpack config`
12 | )
13 | );
14 | process.exit(2);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/internals/scripts/CheckPortInUse.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 | import detectPort from 'detect-port';
3 |
4 | (function CheckPortInUse() {
5 | const port = process.env.PORT || '1212';
6 |
7 | detectPort(port, (err, availablePort) => {
8 | if (port !== String(availablePort)) {
9 | throw new Error(
10 | chalk.whiteBright.bgRed.bold(
11 | `Port "${port}" on "localhost" is already in use. Please use another port. ex: PORT=4343 yarn dev`
12 | )
13 | );
14 | } else {
15 | process.exit(0);
16 | }
17 | });
18 | })();
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "SeeQL",
3 | "productName": "SeeQL",
4 | "version": "0.0.1",
5 | "description": "SeeQL is a database viz tool #TODO",
6 | "scripts": {
7 | "build": "concurrently \"yarn build-main\" \"yarn build-renderer\"",
8 | "build-dll": "cross-env NODE_ENV=development webpack --config ./configs/webpack.config.renderer.dev.dll.babel.js --colors",
9 | "build-e2e": "cross-env E2E_BUILD=true yarn build",
10 | "build-main": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.main.prod.babel.js --colors",
11 | "build-renderer": "cross-env NODE_ENV=production webpack --config ./configs/webpack.config.renderer.prod.babel.js --colors",
12 | "dev": "cross-env START_HOT=1 NODE_ENV=development node -r @babel/register ./internals/scripts/CheckPortInUse.js && cross-env START_HOT=1 yarn start-renderer-dev",
13 | "package": "yarn build && electron-builder build --publish never",
14 | "package-all": "yarn build && electron-builder build -mwl",
15 | "package-ci": "yarn postinstall && yarn build && electron-builder --publish always",
16 | "package-linux": "yarn build && electron-builder build --linux",
17 | "package-win": "yarn build && electron-builder build --win --x64",
18 | "postinstall": "electron-builder install-app-deps package.json && yarn build-dll && opencollective-postinstall",
19 | "prestart": "yarn build",
20 | "start": "cross-env NODE_ENV=production electron ./app/main.prod.js",
21 | "start-main-dev": "cross-env HOT=1 NODE_ENV=development electron ./app/main.dev.babel.js",
22 | "start-renderer-dev": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.renderer.dev.babel.js",
23 | "test": "cross-env NODE_ENV=test BABEL_DISABLE_CACHE=1 jest",
24 | "test-all": "yarn lint && yarn build && yarn test && yarn build-e2e && yarn test-e2e",
25 | "test-e2e": "node -r @babel/register ./internals/scripts/CheckBuiltsExist.js && cross-env NODE_ENV=test testcafe electron:./ ./test/e2e/HomePage.e2e.ts",
26 | "test-e2e-live": "node -r @babel/register ./internals/scripts/CheckBuiltsExist.js && cross-env NODE_ENV=test testcafe-live electron:./ ./test/e2e/HomePage.e2e.ts",
27 | "test-watch": "yarn test --watch",
28 | "proddebug": "rm -rf app/dist release && yarn run package && open release/mac/SeeQL.app",
29 | "eslint": "./node_modules/eslint/bin/eslint.js '**/*.{ts,tsx}' --fix"
30 | },
31 | "main": "./app/main.prod.js",
32 | "build": {
33 | "productName": "SeeQL",
34 | "appId": "org.develar.Seeql.todo",
35 | "asar": false,
36 | "files": [
37 | "app/dist/",
38 | "app/app.html",
39 | "app/main.prod.js",
40 | "app/main.prod.js.map",
41 | "package.json"
42 | ],
43 | "dmg": {
44 | "contents": [
45 | {
46 | "x": 130,
47 | "y": 220
48 | },
49 | {
50 | "x": 410,
51 | "y": 220,
52 | "type": "link",
53 | "path": "/Applications"
54 | }
55 | ]
56 | },
57 | "win": {
58 | "target": [
59 | "nsis",
60 | "msi"
61 | ]
62 | },
63 | "linux": {
64 | "target": [
65 | "deb",
66 | "rpm",
67 | "snap",
68 | "AppImage"
69 | ],
70 | "category": "Development"
71 | },
72 | "directories": {
73 | "buildResources": "resources",
74 | "output": "release"
75 | },
76 | "publish": {
77 | "provider": "github",
78 | "owner": "oslabs-beta",
79 | "repo": "seeql",
80 | "private": false
81 | }
82 | },
83 | "repository": {
84 | "type": "git",
85 | "url": "git+https://github.com/oslabs-beta/seeql.git"
86 | },
87 | "author": {
88 | "name": "SeeQL",
89 | "email": "seeql-labs@todo.com",
90 | "url": "https://seeql.todo.org"
91 | },
92 | "contributors": [
93 | {
94 | "name": "Kate Matthews",
95 | "email": "#TODO",
96 | "url": "#TODO"
97 | },
98 | {
99 | "name": "Alice Wong",
100 | "email": "",
101 | "url": ""
102 | },
103 | {
104 | "name": "Tyler Sayles",
105 | "email": "",
106 | "url": ""
107 | },
108 | {
109 | "name": "Ariel Hyman",
110 | "email": "",
111 | "url": ""
112 | }
113 | ],
114 | "license": "MIT",
115 | "bugs": {
116 | "url": "https://github.com/oslabs-beta/seeql/issues"
117 | },
118 | "keywords": [
119 | "postgres",
120 | "electron",
121 | "react",
122 | "typescript"
123 | ],
124 | "homepage": "https://github.com/oslabs-beta/seeql#readme",
125 | "jest": {
126 | "testURL": "http://localhost/",
127 | "moduleNameMapper": {
128 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/internals/mocks/fileMock.js",
129 | "\\.(css|less|sass|scss)$": "identity-obj-proxy"
130 | },
131 | "moduleFileExtensions": [
132 | "js",
133 | "ts",
134 | "tsx",
135 | "json"
136 | ],
137 | "transform": {
138 | "^.+\\.[jt]sx?$": "babel-jest"
139 | },
140 | "testRegex": "test/.*(spec|test).(ts|tsx|js)$",
141 | "setupFiles": [
142 | "./internals/scripts/CheckBuiltsExist.js"
143 | ]
144 | },
145 | "devDependencies": {
146 | "@babel/core": "^7.2.2",
147 | "@babel/plugin-proposal-class-properties": "^7.2.3",
148 | "@babel/plugin-proposal-decorators": "^7.2.3",
149 | "@babel/plugin-proposal-do-expressions": "^7.2.0",
150 | "@babel/plugin-proposal-export-default-from": "^7.2.0",
151 | "@babel/plugin-proposal-export-namespace-from": "^7.2.0",
152 | "@babel/plugin-proposal-function-bind": "^7.2.0",
153 | "@babel/plugin-proposal-function-sent": "^7.2.0",
154 | "@babel/plugin-proposal-json-strings": "^7.2.0",
155 | "@babel/plugin-proposal-logical-assignment-operators": "^7.2.0",
156 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.2.0",
157 | "@babel/plugin-proposal-numeric-separator": "^7.2.0",
158 | "@babel/plugin-proposal-optional-chaining": "^7.2.0",
159 | "@babel/plugin-proposal-pipeline-operator": "^7.2.0",
160 | "@babel/plugin-proposal-throw-expressions": "^7.2.0",
161 | "@babel/plugin-syntax-dynamic-import": "^7.2.0",
162 | "@babel/plugin-syntax-import-meta": "^7.2.0",
163 | "@babel/plugin-transform-react-constant-elements": "^7.2.0",
164 | "@babel/plugin-transform-react-inline-elements": "^7.2.0",
165 | "@babel/preset-env": "^7.2.3",
166 | "@babel/preset-react": "^7.0.0",
167 | "@babel/preset-typescript": "^7.1.0",
168 | "@babel/register": "^7.0.0",
169 | "@types/electron": "^1.6.10",
170 | "@types/jest": "^24.0.13",
171 | "@types/mocha": "^5.2.7",
172 | "@types/node": "^12.0.7",
173 | "@types/react": "^16.7.18",
174 | "@types/react-dom": "^16.0.11",
175 | "@types/react-router-dom": "^4.3.1",
176 | "@types/webpack-env": "^1.13.9",
177 | "@typescript-eslint/eslint-plugin": "^1.10.2",
178 | "babel-core": "7.0.0-bridge.0",
179 | "babel-eslint": "^10.0.1",
180 | "babel-jest": "^23.6.0",
181 | "babel-loader": "^8.0.5",
182 | "babel-plugin-dev-expression": "^0.2.1",
183 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
184 | "babel-plugin-transform-react-remove-prop-types": "^0.4.21",
185 | "chalk": "^2.4.1",
186 | "concurrently": "^4.1.0",
187 | "connected-react-router": "^6.1.0",
188 | "core-js": "3",
189 | "cross-env": "^5.2.0",
190 | "cross-spawn": "^6.0.5",
191 | "css-loader": "^2.1.0",
192 | "detect-port": "^1.3.0",
193 | "electron": "^4.0.1",
194 | "electron-builder": "^20.38.4",
195 | "electron-devtools-installer": "^2.2.4",
196 | "enzyme": "^3.8.0",
197 | "enzyme-adapter-react-16": "^1.7.1",
198 | "enzyme-to-json": "^3.3.5",
199 | "eslint": "^5.16.0",
200 | "eslint-config-airbnb": "^17.1.0",
201 | "eslint-config-prettier": "^5.0.0",
202 | "eslint-formatter-pretty": "^2.0.0",
203 | "eslint-import-resolver-webpack": "^0.10.1",
204 | "eslint-plugin-compat": "^2.6.3",
205 | "eslint-plugin-import": "^2.14.0",
206 | "eslint-plugin-jest": "^22.1.2",
207 | "eslint-plugin-jsx-a11y": "6.1.2",
208 | "eslint-plugin-prettier": "^3.1.0",
209 | "eslint-plugin-promise": "^4.0.1",
210 | "eslint-plugin-react": "^7.12.3",
211 | "eslint-plugin-testcafe": "^0.2.1",
212 | "fbjs-scripts": "^1.0.1",
213 | "file-loader": "^3.0.1",
214 | "husky": "^1.3.1",
215 | "identity-obj-proxy": "^3.0.0",
216 | "jest": "^23.6.0",
217 | "lint-staged": "^8.1.0",
218 | "mini-css-extract-plugin": "^0.5.0",
219 | "node-sass": "^4.11.0",
220 | "opencollective-postinstall": "^2.0.1",
221 | "optimize-css-assets-webpack-plugin": "^5.0.1",
222 | "prettier": "^1.18.2",
223 | "prettier-eslint-cli": "^4.7.1",
224 | "react-test-renderer": "^16.7.0",
225 | "rimraf": "^2.6.3",
226 | "sass-loader": "^7.1.0",
227 | "sinon": "^7.2.2",
228 | "spectron": "^5.0.0",
229 | "style-loader": "^0.23.1",
230 | "stylelint": "^9.9.0",
231 | "stylelint-config-prettier": "^4.0.0",
232 | "stylelint-config-standard": "^18.2.0",
233 | "terser-webpack-plugin": "^1.2.1",
234 | "testcafe": "^0.23.3",
235 | "testcafe-browser-provider-electron": "^0.0.8",
236 | "testcafe-live": "^0.1.4",
237 | "testcafe-react-selectors": "^3.0.2",
238 | "ts-loader": "^5.3.2",
239 | "typescript": "^3.2.2",
240 | "url-loader": "^1.1.2",
241 | "webpack": "^4.35.0",
242 | "webpack-bundle-analyzer": "^3.0.3",
243 | "webpack-cli": "^3.2.0",
244 | "webpack-dev-server": "^3.1.14",
245 | "webpack-merge": "^4.2.1",
246 | "yarn": "^1.12.3"
247 | },
248 | "dependencies": {
249 | "@babel/plugin-transform-runtime": "^7.4.4",
250 | "@babel/runtime": "^7.4.5",
251 | "@fortawesome/fontawesome-free": "^5.6.3",
252 | "@hot-loader/react-dom": "^16.8.6",
253 | "@types/pg": "^7.4.14",
254 | "@types/styled-components": "^4.1.16",
255 | "@typescript-eslint/parser": "^1.10.2",
256 | "devtron": "^1.4.0",
257 | "electron-debug": "^2.0.0",
258 | "electron-log": "^2.2.17",
259 | "electron-updater": "^4.0.6",
260 | "eslint-config-react-app": "^4.0.1",
261 | "eslint-plugin-flowtype": "^3.10.3",
262 | "eslint-plugin-react-hooks": "^1.6.0",
263 | "fork-ts-checker-webpack-plugin": "^1.3.7",
264 | "grommet": "^2.7.1",
265 | "grommet-icons": "^4.2.0",
266 | "grommet-styles": "^0.2.0",
267 | "history": "^4.7.2",
268 | "pg": "^7.11.0",
269 | "react": "^16.7.0",
270 | "react-dom": "npm:@hot-loader/react-dom",
271 | "react-hot-loader": "^4.6.3",
272 | "react-router": "^4.3.1",
273 | "react-router-dom": "^4.3.1",
274 | "react-spring": "^8.0.23",
275 | "react-table": "^6.10.0",
276 | "rxjs-compat": "^6.5.2",
277 | "source-map-support": "^0.5.9",
278 | "styled-components": "^4.3.0"
279 | },
280 | "devEngines": {
281 | "node": ">=7.x",
282 | "npm": ">=4.x",
283 | "yarn": ">=0.21.3"
284 | },
285 | "browserslist": "electron 1.6"
286 | }
287 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "rangeStrategy": "bump",
4 | "baseBranches": ["next"],
5 | "automerge": true,
6 | "major": { "automerge": false }
7 | }
8 |
--------------------------------------------------------------------------------
/resources/icons/seeqlicon.png_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/icons/seeqlicon.png_128x128.png
--------------------------------------------------------------------------------
/resources/icons/seeqlicon.png_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/icons/seeqlicon.png_16x16.png
--------------------------------------------------------------------------------
/resources/icons/seeqlicon.png_24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/icons/seeqlicon.png_24x24.png
--------------------------------------------------------------------------------
/resources/icons/seeqlicon.png_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/icons/seeqlicon.png_256x256.png
--------------------------------------------------------------------------------
/resources/icons/seeqlicon.png_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/icons/seeqlicon.png_32x32.png
--------------------------------------------------------------------------------
/resources/icons/seeqlicon.png_48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/icons/seeqlicon.png_48x48.png
--------------------------------------------------------------------------------
/resources/icons/seeqlicon.png_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/icons/seeqlicon.png_64x64.png
--------------------------------------------------------------------------------
/resources/icons/seeqlicon.png_96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/icons/seeqlicon.png_96x96.png
--------------------------------------------------------------------------------
/resources/seeqlicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/seeqlicon.png
--------------------------------------------------------------------------------
/resources/seeqlicon.png.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/seeqlicon.png.icns
--------------------------------------------------------------------------------
/resources/seeqlicon.png.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/seeql/cecd3ca084373234502f03e5889ef8cc6710846c/resources/seeqlicon.png.ico
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "plugin:testcafe/recommended",
3 | "env": {
4 | "jest/globals": true
5 | },
6 | "plugins": ["jest", "testcafe"],
7 | "rules": {
8 | "jest/no-disabled-tests": "warn",
9 | "jest/no-focused-tests": "error",
10 | "jest/no-identical-title": "error"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/actions/__snapshots__/counter.spec.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`actions should be gorgeous 1`] = `
4 | Object {
5 | "payload": Object {
6 | "tablename": undefined,
7 | },
8 | "type": "REMOVE_FROM_PINNED",
9 | }
10 |
11 | `;
12 | exports[`actions should decrement should create decrement action 1`] = `
13 | Object {
14 | "type": "DECREMENT_COUNTER",
15 | }
16 | `;
17 |
18 | exports[`actions should increment should create increment action 1`] = `
19 | Object {
20 | "type": "INCREMENT_COUNTER",
21 | }
22 | `;
23 |
24 |
--------------------------------------------------------------------------------
/test/actions/counter.spec.ts:
--------------------------------------------------------------------------------
1 | // import { spy } from 'sinon';
2 | import * as actions from '../../app/actions/actions';
3 |
4 | describe('actions', () => {
5 | it('should be gorgeous', () => {
6 | let x;
7 | expect(actions.removeFromPinned(x)).toMatchSnapshot();
8 | });
9 | });
10 |
11 | // describe('actions', () => {
12 | // it('should increment should create increment action', () => {
13 | // expect(actions.increment()).toMatchSnapshot();
14 | // });
15 |
16 | // it('should decrement should create decrement action', () => {
17 | // expect(actions.decrement()).toMatchSnapshot();
18 | // });
19 |
20 | // it('should incrementIfOdd should create increment action', () => {
21 | // const fn = actions.incrementIfOdd();
22 | // expect(fn).toBeInstanceOf(Function);
23 | // const dispatch = spy();
24 | // const getState = () => ({ counter: 1 });
25 | // fn(dispatch, getState);
26 | // expect(
27 | // dispatch.calledWith({ type: actions.CounterTypeKeys.INCREMENT_COUNTER })
28 | // ).toBe(true);
29 | // });
30 |
31 | // it('should incrementIfOdd shouldnt create increment action if counter is even', () => {
32 | // const fn = actions.incrementIfOdd();
33 | // const dispatch = spy();
34 | // const getState = () => ({ counter: 2 });
35 | // fn(dispatch, getState);
36 | // expect(dispatch.called).toBe(false);
37 | // });
38 |
39 | // // There's no nice way to test this at the moment...
40 | // it('should incrementAsync', done => {
41 | // const fn = actions.incrementAsync(1);
42 | // expect(fn).toBeInstanceOf(Function);
43 | // const dispatch = spy();
44 | // fn(dispatch);
45 | // setTimeout(() => {
46 | // expect(
47 | // dispatch.calledWith({
48 | // type: actions.CounterTypeKeys.INCREMENT_COUNTER
49 | // })
50 | // ).toBe(true);
51 | // done();
52 | // }, 5);
53 | // });
54 | // });
55 |
--------------------------------------------------------------------------------
/test/components/Counter.spec.tsx:
--------------------------------------------------------------------------------
1 | // import { spy } from 'sinon';
2 | // import * as React from 'react';
3 | // import Enzyme, { shallow } from 'enzyme';
4 | // import Adapter from 'enzyme-adapter-react-16';
5 | // import { BrowserRouter as Router } from 'react-router-dom';
6 | // import renderer from 'react-test-renderer';
7 | // import Counter from '../../app/components/Counter';
8 |
9 | // Enzyme.configure({ adapter: new Adapter() });
10 |
11 | // function setup() {
12 | // const actions = {
13 | // increment: spy(),
14 | // incrementIfOdd: spy(),
15 | // incrementAsync: spy(),
16 | // decrement: spy()
17 | // };
18 | // const component = shallow();
19 | // return {
20 | // component,
21 | // actions,
22 | // buttons: component.find('button'),
23 | // p: component.find('.counter')
24 | // };
25 | // }
26 |
27 | // describe('Counter component', () => {
28 | // it('should should display count', () => {
29 | // const { p } = setup();
30 | // expect(p.text()).toMatch(/^1$/);
31 | // });
32 |
33 | // it('should first button should call increment', () => {
34 | // const { buttons, actions } = setup();
35 | // buttons.at(0).simulate('click');
36 | // expect(actions.increment.called).toBe(true);
37 | // });
38 |
39 | // it('should match exact snapshot', () => {
40 | // const { actions } = setup();
41 | // const counter = (
42 | //
43 | //
44 | //
45 | //
46 | //
47 | // );
48 | // const tree = renderer.create(counter).toJSON();
49 |
50 | // expect(tree).toMatchSnapshot();
51 | // });
52 |
53 | // it('should second button should call decrement', () => {
54 | // const { buttons, actions } = setup();
55 | // buttons.at(1).simulate('click');
56 | // expect(actions.decrement.called).toBe(true);
57 | // });
58 |
59 | // it('should third button should call incrementIfOdd', () => {
60 | // const { buttons, actions } = setup();
61 | // buttons.at(2).simulate('click');
62 | // expect(actions.incrementIfOdd.called).toBe(true);
63 | // });
64 |
65 | // it('should fourth button should call incrementAsync', () => {
66 | // const { buttons, actions } = setup();
67 | // buttons.at(3).simulate('click');
68 | // expect(actions.incrementAsync.called).toBe(true);
69 | // });
70 | // });
71 |
--------------------------------------------------------------------------------
/test/components/__snapshots__/Counter.spec.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Counter component should match exact snapshot 1`] = `
4 |
5 |
6 |
19 |
23 | 1
24 |
25 |
28 |
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 |
--------------------------------------------------------------------------------