├── assets └── icons │ ├── png │ ├── 16x16.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 64x64.png │ ├── 128x128.png │ └── 256x256.png │ ├── win │ └── seeqr.ico │ └── mac │ └── seeqr.icns ├── frontend ├── assets │ ├── images │ │ ├── data.png │ │ ├── history.png │ │ ├── query1.png │ │ ├── query2.png │ │ ├── results.png │ │ ├── serenakuo.png │ │ ├── comparisons.png │ │ ├── franknorton.png │ │ ├── logo_color.png │ │ ├── logo_readme.png │ │ ├── muhammadtrad.png │ │ ├── queryinput.png │ │ ├── schemamodal.png │ │ ├── seeqr_dock.png │ │ ├── catherinechiu.png │ │ ├── mercerstronck.png │ │ ├── queryruntime1.png │ │ ├── queryruntime2.png │ │ ├── wholeinterface.png │ │ ├── logo_monochrome.png │ │ └── splash_screencap.png │ └── stylesheets │ │ ├── css │ │ ├── variables.css │ │ ├── modal.css │ │ ├── components.css │ │ ├── layout.css │ │ └── style.css │ │ └── scss │ │ ├── variables.scss │ │ ├── modal.scss │ │ ├── layout.scss │ │ ├── components.scss │ │ └── style.scss ├── index.tsx └── components │ ├── rightPanel │ ├── tabsChildren │ │ └── Tab.tsx │ ├── schemaChildren │ │ ├── Data.tsx │ │ ├── GenerateData.tsx │ │ ├── SchemaInput.tsx │ │ ├── dataChildren │ │ │ └── DataTable.tsx │ │ ├── Query.tsx │ │ ├── Results.tsx │ │ └── SchemaModal.tsx │ ├── SchemaContainer.tsx │ └── Tabs.tsx │ ├── Splash.tsx │ ├── leftPanel │ ├── History.tsx │ └── Compare.tsx │ ├── App.tsx │ └── MainPanel.tsx ├── .gitignore ├── docker-compose.yml ├── LICENSE ├── backend ├── models.ts ├── mainMenu.ts ├── main.ts └── channels.ts ├── package.json ├── webpack.config.js ├── tsconfig.json └── README.md /assets/icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/assets/icons/png/16x16.png -------------------------------------------------------------------------------- /assets/icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/assets/icons/png/32x32.png -------------------------------------------------------------------------------- /assets/icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/assets/icons/png/48x48.png -------------------------------------------------------------------------------- /assets/icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/assets/icons/png/64x64.png -------------------------------------------------------------------------------- /assets/icons/win/seeqr.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/assets/icons/win/seeqr.ico -------------------------------------------------------------------------------- /assets/icons/mac/seeqr.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/assets/icons/mac/seeqr.icns -------------------------------------------------------------------------------- /assets/icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/assets/icons/png/128x128.png -------------------------------------------------------------------------------- /assets/icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/assets/icons/png/256x256.png -------------------------------------------------------------------------------- /frontend/assets/images/data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/data.png -------------------------------------------------------------------------------- /frontend/assets/images/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/history.png -------------------------------------------------------------------------------- /frontend/assets/images/query1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/query1.png -------------------------------------------------------------------------------- /frontend/assets/images/query2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/query2.png -------------------------------------------------------------------------------- /frontend/assets/images/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/results.png -------------------------------------------------------------------------------- /frontend/assets/images/serenakuo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/serenakuo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.vscode 3 | /dist 4 | /tsCompiled 5 | */.DS_Store 6 | .DS_Store 7 | dvdrental.tar 8 | package-lock.json -------------------------------------------------------------------------------- /frontend/assets/images/comparisons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/comparisons.png -------------------------------------------------------------------------------- /frontend/assets/images/franknorton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/franknorton.png -------------------------------------------------------------------------------- /frontend/assets/images/logo_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/logo_color.png -------------------------------------------------------------------------------- /frontend/assets/images/logo_readme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/logo_readme.png -------------------------------------------------------------------------------- /frontend/assets/images/muhammadtrad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/muhammadtrad.png -------------------------------------------------------------------------------- /frontend/assets/images/queryinput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/queryinput.png -------------------------------------------------------------------------------- /frontend/assets/images/schemamodal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/schemamodal.png -------------------------------------------------------------------------------- /frontend/assets/images/seeqr_dock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/seeqr_dock.png -------------------------------------------------------------------------------- /frontend/assets/images/catherinechiu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/catherinechiu.png -------------------------------------------------------------------------------- /frontend/assets/images/mercerstronck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/mercerstronck.png -------------------------------------------------------------------------------- /frontend/assets/images/queryruntime1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/queryruntime1.png -------------------------------------------------------------------------------- /frontend/assets/images/queryruntime2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/queryruntime2.png -------------------------------------------------------------------------------- /frontend/assets/images/wholeinterface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/wholeinterface.png -------------------------------------------------------------------------------- /frontend/assets/images/logo_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/logo_monochrome.png -------------------------------------------------------------------------------- /frontend/assets/images/splash_screencap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catherinechiu/SeeQR/HEAD/frontend/assets/images/splash_screencap.png -------------------------------------------------------------------------------- /frontend/assets/stylesheets/css/variables.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); 3 | -------------------------------------------------------------------------------- /frontend/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { App } from './components/App'; 4 | import './assets/stylesheets/css/style.css'; 5 | import 'codemirror/lib/codemirror.css'; 6 | 7 | const root = document.createElement('div'); 8 | root.id = 'root'; 9 | document.body.appendChild(root); 10 | 11 | render( 12 |
13 | 14 |
, 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/scss/variables.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=PT+Mono&display=swap'); 3 | 4 | // typography 5 | $font-stack: 'PT Sans', sans-serif; 6 | $font-input: 'PT Mono', monospace; 7 | $p-weight: 100; 8 | $title-weight: 300; 9 | $default-text: 1em; 10 | 11 | // colors 12 | $background-darkmode: #2b2d35; 13 | $background-modal-darkmode: #30353a; 14 | $background-lightmode: #9abacc; 15 | $primary-color-lightmode: #1a1a1a; 16 | $primary-color-darkmode: #c6d2d5; 17 | $border-darkmode: #444c50; 18 | $button-darkmode: #596368; 19 | $background-darkmode-darker: #292a30; 20 | $mint-green: #6cbba9; 21 | -------------------------------------------------------------------------------- /frontend/components/rightPanel/tabsChildren/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | type TabProps = { 4 | onClickTabItem: any, 5 | currentSchema: string, 6 | label: string, 7 | }; 8 | export class Tab extends Component { 9 | 10 | render() { 11 | const { 12 | onClickTabItem, 13 | currentSchema, 14 | label, 15 | } = this.props; 16 | 17 | let className = "tab-list-item"; 18 | if (currentSchema === label) { 19 | className += " tab-list-active"; 20 | } 21 | 22 | return ( 23 |
  • onClickTabItem(label)}> 24 | {label} 25 |
  • 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | bb: 4 | image: busybox 5 | volumes: 6 | - database-data:/var/lib/postgresql/data 7 | ports: 8 | - 5001:5001 9 | container_name: busybox-1 10 | db: 11 | image: postgres:12 12 | environment: 13 | POSTGRES_PASSWORD: postgres 14 | POSTGRES_USER: postgres 15 | POSTGRES_DB: defaultDB 16 | volumes: 17 | - database-data 18 | depends_on: 19 | - bb 20 | container_name: postgres-1 21 | ports: 22 | - 5432:5432 23 | hostname: localhost 24 | networks: 25 | - default 26 | 27 | volumes: 28 | database-data: # named volumes can be managed easier using docker-compose -------------------------------------------------------------------------------- /frontend/components/rightPanel/schemaChildren/Data.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Table } from './dataChildren/DataTable'; 3 | 4 | type DataProps = { 5 | queries: { 6 | queryString: string; 7 | queryData: {}[]; 8 | queryStatistics: any 9 | querySchema: string; 10 | queryLabel: string; 11 | }[]; 12 | }; 13 | 14 | export class Data extends Component { 15 | constructor(props) { 16 | super(props); 17 | } 18 | 19 | render() { 20 | const { queries } = this.props; 21 | 22 | return ( 23 |
    24 |
    25 |
    26 |
    27 |
    28 |

    Data Table

    29 |
    30 | {queries.length === 0 ? null : } 31 | 32 | 33 | 34 | ); 35 | } 36 | } -------------------------------------------------------------------------------- /frontend/components/Splash.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component, MouseEvent } from 'react'; 2 | 3 | type SplashProps = { 4 | openSplash: boolean; 5 | handleFileClick: any; 6 | handleSkipClick: any; 7 | }; 8 | 9 | export class Splash extends Component { 10 | // a dialogue menu with retrieve the file path 11 | constructor(props: SplashProps) { 12 | super(props); 13 | } 14 | 15 | render() { 16 | return ( 17 |
    18 |
    19 |
    20 |

    Welcome!

    21 |

    Import database in .sql or .tar?

    22 |
    23 |
    24 | 25 | 26 |
    27 |
    28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/components/rightPanel/SchemaContainer.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Data } from './schemaChildren/Data'; 3 | import { Results } from './schemaChildren/Results'; 4 | import Query from './schemaChildren/Query'; 5 | 6 | type SchemaContainerProps = { 7 | queries: any; 8 | currentSchema: string; 9 | }; 10 | 11 | type state = { 12 | currentSchema: string; 13 | }; 14 | 15 | export class SchemaContainer extends Component { 16 | constructor(props: SchemaContainerProps) { 17 | super(props); 18 | } 19 | 20 | state: state = { 21 | currentSchema: '', 22 | }; 23 | 24 | render() { 25 | return ( 26 |
    27 |
    28 |
    29 | 30 | 31 |
    32 |
    33 | 34 |
    35 |
    36 |
    37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Serena Kuo 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 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/css/modal.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); 3 | .modal { 4 | width: 615px; 5 | height: 700px; 6 | background-color: #30353a; 7 | border: 0.5px solid #1a1a1a; 8 | transition: 1.1s ease-out; 9 | box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); 10 | filter: blur(0); 11 | transform: scale(1); 12 | opacity: 1; 13 | visibility: visible; 14 | padding: 40px; 15 | z-index: 1010; 16 | position: fixed; 17 | top: 100px; 18 | } 19 | 20 | .modal button { 21 | padding: 10px; 22 | margin: 10px 0; 23 | } 24 | 25 | .modal.off { 26 | opacity: 0; 27 | visibility: hidden; 28 | filter: blur(8px); 29 | transform: scale(0.33); 30 | box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); 31 | } 32 | 33 | .modal h2 { 34 | border-bottom: 1px solid #ccc; 35 | padding: 1rem; 36 | margin: 0; 37 | } 38 | 39 | .modal .content { 40 | padding: 1rem; 41 | } 42 | 43 | .modal input { 44 | margin: 10px 0; 45 | display: block; 46 | color: #6cbba9; 47 | font-family: "PT Mono", monospace; 48 | } 49 | 50 | .schema-text-field { 51 | height: 300px; 52 | width: 600px; 53 | } 54 | 55 | .modal-buttons { 56 | display: flex; 57 | flex-direction: row; 58 | } 59 | 60 | .modal-buttons .input-button { 61 | margin-left: 15px; 62 | } 63 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/scss/modal.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | .modal { 3 | width: 615px; 4 | height: 700px; 5 | background-color: $background-modal-darkmode; 6 | border: 0.5px solid $primary-color-lightmode; 7 | transition: 1.1s ease-out; 8 | box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); 9 | filter: blur(0); 10 | transform: scale(1); 11 | opacity: 1; 12 | visibility: visible; 13 | padding: 40px; 14 | z-index: 1010; 15 | position: fixed; 16 | top: 100px; 17 | button { 18 | padding: 10px; 19 | margin: 10px 0; 20 | } 21 | } 22 | .modal.off { 23 | opacity: 0; 24 | visibility: hidden; 25 | filter: blur(8px); 26 | transform: scale(0.33); 27 | box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); 28 | } 29 | // @supports (offset-rotation: 0deg) { 30 | // offset-rotation: 0deg; 31 | // offset-path: path("M 250,100 S -300,500 -700,-200"); 32 | // .modal.off { 33 | // offset-distance: 100%; 34 | // } 35 | // } 36 | // @media (prefers-reduced-motion) { 37 | // .modal { 38 | // offset-path: none; 39 | // } 40 | // } 41 | .modal h2 { 42 | border-bottom: 1px solid #ccc; 43 | padding: 1rem; 44 | margin: 0; 45 | } 46 | .modal .content { 47 | padding: 1rem; 48 | } 49 | .modal input { 50 | margin: 10px 0; 51 | display: block; 52 | color: $mint-green; 53 | font-family: $font-input; 54 | } 55 | .schema-text-field { 56 | height: 300px; 57 | width: 600px; 58 | } 59 | .modal-buttons { 60 | display: flex; 61 | flex-direction: row; 62 | .input-button { 63 | margin-left: 15px; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/components/rightPanel/schemaChildren/GenerateData.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const { ipcRenderer } = window.require('electron'); 4 | 5 | type GenerateDataProps = { 6 | onClose: any; 7 | }; 8 | 9 | type state = {}; 10 | 11 | class GenerateData extends Component { 12 | constructor(props: GenerateDataProps) { 13 | super(props); 14 | this.handleFormSubmit = this.handleFormSubmit.bind(this); 15 | } 16 | 17 | state: state = {}; 18 | 19 | handleFormSubmit(event: any) { 20 | event.preventDefault(); 21 | // pass down any state from the form 22 | const formObj = { 23 | }; 24 | // on submit button click, sends form obj to backend 25 | ipcRenderer.send('form-input', formObj); 26 | } 27 | 28 | // close modal function 29 | onClose = (event: any) => { 30 | this.props.onClose && this.props.onClose(event); 31 | }; 32 | 33 | // input all form input fields under "form" and link to event handlers to save to state 34 | // bind all functions for field entries on the form 35 | render() { 36 | return ( 37 |
    38 |
    39 | 47 | 48 |
    49 | ); 50 | } 51 | } 52 | 53 | export default GenerateData; 54 | -------------------------------------------------------------------------------- /frontend/components/leftPanel/History.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | type HistoryProps = { 4 | queries: { 5 | queryString: string; 6 | queryData: {}[]; 7 | queryStatistics: any 8 | querySchema: string; 9 | queryLabel: string; 10 | }[]; 11 | currentSchema: string; 12 | }; 13 | 14 | export class History extends Component { 15 | constructor(props: HistoryProps) { 16 | super(props); 17 | } 18 | 19 | renderTableHistory() { 20 | return this.props.queries.map((query, index) => { 21 | const { queryStatistics, querySchema, queryLabel } = query; 22 | 23 | const { ['QUERY PLAN']: queryPlan } = queryStatistics[0]; 24 | 25 | const { 26 | Plan, 27 | ['Planning Time']: planningTime, 28 | ['Execution Time']: executionTime, 29 | } = queryPlan[0]; 30 | const { ['Actual Rows']: actualRows, ['Actual Total Time']: actualTotalTime } = Plan; 31 | 32 | return ( 33 |
    34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }); 41 | } 42 | 43 | render() { 44 | const { queries } = this.props; 45 | 46 | return ( 47 |
    48 |

    History

    49 |
    50 |
    {queryLabel}{querySchema}{actualRows}{actualTotalTime}
    51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {this.renderTableHistory()} 59 | 60 |
    {'Query Label'}{'Schema'}{'Total Rows'}{'Total Time'}
    61 |
    62 |
    63 | ); 64 | } 65 | } 66 | 67 | export default History; 68 | -------------------------------------------------------------------------------- /backend/models.ts: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | 3 | // Initialize to a default db. 4 | // URI Format: postgres://username:password@hostname:port/databasename 5 | let PG_URI: string = 'postgres://postgres:postgres@localhost:5432/defaultDB'; 6 | let pool: any = new Pool({ connectionString: PG_URI }); 7 | 8 | module.exports = { 9 | query: (text, params, callback) => { 10 | console.log('Executed query: ', text); 11 | return pool.query(text, params, callback); 12 | }, 13 | changeDB: (dbName: string) => { 14 | PG_URI = 'postgres://postgres:postgres@localhost:5432/' + dbName; 15 | pool = new Pool({ connectionString: PG_URI }); 16 | console.log('Current URI: ', PG_URI); 17 | }, 18 | getLists: () => { 19 | return new Promise((resolve) => { 20 | const listObj = { 21 | tableList: [], // current database's tables 22 | databaseList: [], 23 | }; 24 | // This query returns the names of all the tables in the database, so that the frontend can make a visual for the user 25 | pool 26 | .query( 27 | "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;" 28 | ) 29 | .then((tables) => { 30 | let tableList: any = []; 31 | for (let i = 0; i < tables.rows.length; ++i) { 32 | tableList.push(tables.rows[i].table_name); 33 | } 34 | listObj.tableList = tableList; 35 | 36 | pool.query('SELECT datname FROM pg_database;').then((databases) => { 37 | let dbList: any = []; 38 | for (let i = 0; i < databases.rows.length; ++i) { 39 | let curName = databases.rows[i].datname; 40 | if (curName !== 'postgres' && curName !== 'template0' && curName !== 'template1') 41 | dbList.push(databases.rows[i].datname); 42 | } 43 | listObj.databaseList = dbList; 44 | resolve(listObj); 45 | }); 46 | }); 47 | }); 48 | }, 49 | }; -------------------------------------------------------------------------------- /frontend/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Splash } from './Splash'; 3 | import MainPanel from './MainPanel'; 4 | 5 | const { dialog } = require('electron').remote; 6 | const { ipcRenderer } = window.require('electron'); 7 | 8 | type ClickEvent = React.MouseEvent; 9 | 10 | type state = { 11 | openSplash: boolean; 12 | }; 13 | 14 | type AppProps = {}; 15 | 16 | export class App extends Component { 17 | constructor(props: AppProps) { 18 | super(props); 19 | this.handleFileClick = this.handleFileClick.bind(this); 20 | this.handleSkipClick = this.handleSkipClick.bind(this); 21 | } 22 | 23 | state: state = { 24 | openSplash: true, 25 | }; 26 | 27 | handleFileClick(event: ClickEvent) { 28 | dialog 29 | .showOpenDialog( 30 | { 31 | properties: ['openFile'], 32 | filters: [{ name: 'Custom File Type', extensions: ['tar', 'sql'] }], 33 | message: 'Please upload .sql or .tar database file' 34 | }, 35 | ) 36 | .then((result: object) => { 37 | const filePathArr = result["filePaths"]; 38 | // send via channel to main process 39 | if (!result["canceled"]) { 40 | ipcRenderer.send('upload-file', filePathArr); 41 | this.setState({ openSplash: false }); 42 | } 43 | }) 44 | .catch((err: object) => { 45 | console.log(err); 46 | }); 47 | } 48 | 49 | handleSkipClick(event: ClickEvent) { 50 | ipcRenderer.send('skip-file-upload'); 51 | this.setState({ openSplash: false }); 52 | } 53 | 54 | render() { 55 | // listen for menu to invoke handleFileClick 56 | ipcRenderer.on('menu-upload-file', () => { 57 | this.handleFileClick; 58 | }); 59 | 60 | return ( 61 |
    62 | {this.state.openSplash ? ( 63 | 68 | ) : ( 69 | 70 | )} 71 |
    72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /frontend/components/rightPanel/schemaChildren/SchemaInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import GenerateData from './GenerateData'; 3 | 4 | const { ipcRenderer } = window.require('electron'); 5 | 6 | // Codemirror configuration 7 | import 'codemirror/lib/codemirror.css'; // Styline 8 | import 'codemirror/mode/sql/sql'; // Language (Syntax Highlighting) 9 | import 'codemirror/theme/lesser-dark.css'; // Theme 10 | import CodeMirror from 'react-codemirror'; 11 | 12 | type SchemaInputProps = { 13 | onClose: any; 14 | schemaName: string; 15 | }; 16 | 17 | type state = { 18 | schemaEntry: string; 19 | }; 20 | 21 | class SchemaInput extends Component { 22 | constructor(props: SchemaInputProps) { 23 | super(props); 24 | this.handleSchemaSubmit = this.handleSchemaSubmit.bind(this); 25 | this.handleSchemaChange = this.handleSchemaChange.bind(this); 26 | } 27 | 28 | state: state = { 29 | schemaEntry: '', 30 | }; 31 | 32 | // Updates state.schemaEntry as user inputs query string 33 | handleSchemaChange(event: string) { 34 | this.setState({ 35 | schemaEntry: event, 36 | }); 37 | } 38 | 39 | handleSchemaSubmit(event: any) { 40 | event.preventDefault(); 41 | 42 | const schemaObj = { 43 | schemaName: this.props.schemaName, 44 | schemaFilePath: '', 45 | schemaEntry: this.state.schemaEntry, 46 | }; 47 | 48 | ipcRenderer.send('input-schema', schemaObj); 49 | } 50 | 51 | onClose = (event: any) => { 52 | this.props.onClose && this.props.onClose(event); 53 | }; 54 | 55 | render() { 56 | // Codemirror module configuration options 57 | var options = { 58 | lineNumbers: true, 59 | mode: 'sql', 60 | theme: 'lesser-dark', 61 | }; 62 | 63 | return ( 64 |
    65 |
    66 |
    67 |
    68 | this.handleSchemaChange(e)} 70 | options={options} 71 | /> 72 |
    73 | 74 |
    75 |
    76 | ); 77 | } 78 | } 79 | 80 | export default SchemaInput; 81 | -------------------------------------------------------------------------------- /backend/mainMenu.ts: -------------------------------------------------------------------------------- 1 | // if process platform is darwin, operating on mac 2 | const isMac = process.platform === 'darwin'; 3 | 4 | const { app, shell } = require('electron'); 5 | module.exports = [ 6 | // { role: 'appMenu' } 7 | ...(isMac 8 | ? [ 9 | { 10 | label: app.name, 11 | submenu: [ 12 | { role: 'about' }, 13 | { type: 'separator' }, 14 | { role: 'services' }, 15 | { type: 'separator' }, 16 | { role: 'hide' }, 17 | { role: 'hideothers' }, 18 | { role: 'unhide' }, 19 | { type: 'separator' }, 20 | { role: 'quit' }, 21 | ], 22 | }, 23 | ] 24 | : []), 25 | // { role: 'fileMenu' } 26 | { 27 | label: 'File', 28 | submenu: [isMac ? { role: 'close' } : { role: 'quit' }], 29 | }, 30 | // { role: 'editMenu' } 31 | { 32 | label: 'Edit', 33 | submenu: [ 34 | { role: 'undo' }, 35 | { role: 'redo' }, 36 | { type: 'separator' }, 37 | { role: 'cut' }, 38 | { role: 'copy' }, 39 | { role: 'paste' }, 40 | ...(isMac 41 | ? [ 42 | { role: 'delete' }, 43 | { role: 'selectAll' }, 44 | { type: 'separator' }, 45 | { 46 | label: 'Speech', 47 | submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }], 48 | }, 49 | ] 50 | : [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]), 51 | ], 52 | }, 53 | // { role: 'viewMenu' } 54 | { 55 | label: 'View', 56 | submenu: [ 57 | { role: 'reload' }, 58 | { role: 'forcereload' }, 59 | { role: 'toggledevtools' }, 60 | { type: 'separator' }, 61 | { role: 'resetzoom' }, 62 | { role: 'zoomin' }, 63 | { role: 'zoomout' }, 64 | { type: 'separator' }, 65 | { role: 'togglefullscreen' }, 66 | ], 67 | }, 68 | // { role: 'windowMenu' } 69 | { 70 | label: 'Window', 71 | submenu: [ 72 | { role: 'minimize' }, 73 | { role: 'zoom' }, 74 | ...(isMac 75 | ? [{ type: 'separator' }, { role: 'front' }, { type: 'separator' }, { role: 'window' }] 76 | : [{ role: 'close' }]), 77 | ], 78 | }, 79 | { 80 | role: 'help', 81 | submenu: [ 82 | { 83 | label: 'SeeQR Documentation', 84 | click: () => { 85 | shell.openExternal('https://electronjs.org'); 86 | }, 87 | }, 88 | ], 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /frontend/components/rightPanel/schemaChildren/dataChildren/DataTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | type TableProps = { 4 | queries: { 5 | queryString: string; 6 | queryData: {}[]; 7 | queryStatistics: any 8 | querySchema: string; 9 | queryLabel: string; 10 | }[]; 11 | }; 12 | export class Table extends Component { 13 | 14 | constructor(props) { 15 | super(props); 16 | this.getKeys = this.getKeys.bind(this); 17 | this.getHeader = this.getHeader.bind(this); 18 | this.getRowsData = this.getRowsData.bind(this); 19 | } 20 | 21 | // Returns list of headings that should be displayed @ top of table 22 | getKeys() { 23 | const { queries } = this.props; 24 | 25 | // All keys will be consistent across each object in queryData, 26 | // so we only need to list keys of first object in data returned 27 | // from backend. 28 | return Object.keys(queries[queries.length - 1].queryData[0]); 29 | } 30 | 31 | // Create Header by generating a element for each key. 32 | getHeader() { 33 | var keys = this.getKeys(); 34 | return keys.map((key, index) => { 35 | return {key.toUpperCase()} 36 | }) 37 | } 38 | 39 | // Iterate through queryData array to return the body part of the table. 40 | getRowsData() { 41 | const { queries } = this.props; 42 | 43 | var items = queries[queries.length - 1].queryData; 44 | var keys = this.getKeys(); // actor_id, firstName, lastName, lastUpdated 45 | 46 | return items.map((row, index) => { 47 | return 48 | }) 49 | } 50 | 51 | render() { 52 | 53 | return ( 54 |
    55 | 56 | 57 | {this.getHeader()} 58 | 59 | 60 | {this.getRowsData()} 61 | 62 |
    63 |
    64 | 65 | ); 66 | } 67 | } 68 | 69 | type RenderRowProps = { 70 | data: any; 71 | keys: any; 72 | key: any; 73 | }; 74 | 75 | // Returns each cell within table 76 | const RenderRow = (props: RenderRowProps) => { 77 | const { data, keys } = props; 78 | return keys.map((header, index) => { 79 | // if the value of a row is undefined, then go to next iteration 80 | if (data[header] == undefined) return; 81 | // turn all values in data object to string or number 82 | data[header] = data[header].toString(); 83 | return {data[header]} 84 | }) 85 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SeeQR", 3 | "version": "1.0.0", 4 | "description": "SeeQR", 5 | "main": "./tsCompiled/backend/main", 6 | "scripts": { 7 | "build": "tsc && webpack", 8 | "docker": "docker-compose up -d", 9 | "start": "cross-env NODE_ENV=production electron --noDevServer .", 10 | "resetContainer": "docker container stop postgres-1 && docker container prune && docker container ls -a", 11 | "dev": "tsc && cross-env NODE_ENV=development webpack-dev-server --hot", 12 | "new": "cross-env NODE_ENV=production electron --noDevServer ." 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/oslabs-beta/SeeQR" 17 | }, 18 | "keywords": [], 19 | "author": "Team SeeQR", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@skidding/react-codemirror": "^1.0.2", 23 | "autoprefixer": "^9.8.5", 24 | "chart.js": "^2.9.3", 25 | "codemirror": "^5.57.0", 26 | "concurrently": "^5.3.0", 27 | "cross-env": "^7.0.2", 28 | "electron": "^9.0.0", 29 | "electron-store": "^6.0.0", 30 | "faker": "^5.1.0", 31 | "fix-path": "^3.0.0", 32 | "pg": "^8.3.2", 33 | "react": "^16.13.1", 34 | "react-bootstrap": "^1.3.0", 35 | "react-chartjs-2": "^2.10.0", 36 | "react-codemirror": "^1.0.0", 37 | "react-dom": "^16.13.1", 38 | "react-router-dom": "^5.2.0", 39 | "sass": "^1.26.10" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.10.5", 43 | "@babel/preset-env": "^7.10.4", 44 | "@babel/preset-react": "^7.10.4", 45 | "@types/node": "^14.6.0", 46 | "@types/react": "^16.9.46", 47 | "@types/react-dom": "^16.9.8", 48 | "@types/react-router-dom": "^5.1.5", 49 | "babel-loader": "^8.1.0", 50 | "babel-minify-webpack-plugin": "^0.3.1", 51 | "csp-html-webpack-plugin": "^4.0.0", 52 | "css-loader": "^3.5.3", 53 | "electron": "^9.0.0", 54 | "electron-devtools-installer": "^3.0.0", 55 | "electron-packager": "^14.2.1", 56 | "file-loader": "^6.0.0", 57 | "html-webpack-plugin": "^4.3.0", 58 | "image-webpack-loader": "^6.0.0", 59 | "mini-css-extract-plugin": "^0.9.0", 60 | "nodemon": "^2.0.4", 61 | "postcss-loader": "^3.0.0", 62 | "react-router-dom": "^5.2.0", 63 | "sass-loader": "^9.0.2", 64 | "source-map-loader": "^1.0.1", 65 | "style-loader": "^1.2.1", 66 | "ts-loader": "^8.0.2", 67 | "ts-node": "^8.10.2", 68 | "typescript": "^3.9.7", 69 | "webpack": "^4.43.0", 70 | "webpack-cli": "^3.3.12", 71 | "webpack-dev-server": "^3.11.0" 72 | }, 73 | "bugs": { 74 | "url": "https://github.com/oslabs-beta/SeeQR/issues" 75 | }, 76 | "homepage": "https://github.com/oslabs-beta/SeeQR" 77 | } -------------------------------------------------------------------------------- /frontend/components/rightPanel/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { SchemaContainer } from './SchemaContainer'; 3 | import SchemaModal from './schemaChildren/SchemaModal'; 4 | import { Tab } from './tabsChildren/Tab'; 5 | 6 | const { ipcRenderer } = window.require('electron'); 7 | 8 | type TabsProps = { 9 | currentSchema: string, 10 | tabList: string[], 11 | queries: any, 12 | onClickTabItem: any, 13 | } 14 | 15 | type state = { 16 | show: boolean; 17 | }; 18 | export class Tabs extends Component { 19 | constructor(props: TabsProps) { 20 | super(props); 21 | this.showModal = this.showModal.bind(this); 22 | } 23 | state: state = { 24 | show: false, 25 | }; 26 | 27 | showModal = (event: any) => { 28 | this.setState({ show: true }); 29 | }; 30 | 31 | 32 | componentDidMount() { 33 | // After schema is successfully sent to backend, backend spins up new database with inputted schemaName. 34 | // It will send the frontend an updated variable 'lists' that is an array of updated lists of all the tabs (which is the same 35 | // thing as all the databases). We open a channel to listen for it here inside of componendDidMount, then 36 | // we invoke onClose to close schemaModal ONLY after we are sure that backend has created that channel. 37 | ipcRenderer.on('db-lists', (event: any, returnedLists: any) => { 38 | this.onClose(event); 39 | }) 40 | } 41 | 42 | onClose = (event: any) => { 43 | this.setState({ show: false }) 44 | }; 45 | 46 | render() { 47 | const { 48 | onClickTabItem, 49 | tabList, 50 | currentSchema, 51 | queries, 52 | } = this.props; 53 | 54 | const activeTabQueries = queries.filter((query) => query.querySchema === currentSchema); 55 | 56 | return ( 57 |
    58 |
      59 | 60 | {tabList.map((tab, index) => { 61 | return ( 62 | 68 | ); 69 | })} 70 | 71 | 72 | 80 | 81 |
    82 | 83 |
    84 | {tabList.map((tab, index) => { 85 | if (tab !== currentSchema) return undefined; 86 | return ; 87 | })} 88 |
    89 |
    90 | ); 91 | } 92 | } -------------------------------------------------------------------------------- /frontend/components/MainPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Compare } from './leftPanel/Compare'; 3 | import History from './leftPanel/History'; 4 | import { Tabs } from './rightPanel/Tabs'; 5 | 6 | const { ipcRenderer } = window.require('electron'); 7 | 8 | type MainState = { 9 | queries: { 10 | queryString: string; 11 | queryData: {}[]; 12 | queryStatistics: any 13 | querySchema: string; 14 | queryLabel: string; 15 | }[]; 16 | currentSchema: string; 17 | lists: any; 18 | }; 19 | 20 | type MainProps = {}; 21 | class MainPanel extends Component { 22 | constructor(props: MainProps) { 23 | super(props); 24 | this.onClickTabItem = this.onClickTabItem.bind(this); 25 | } 26 | state: MainState = { 27 | queries: [], 28 | // currentSchema will change depending on which Schema Tab user selects 29 | currentSchema: 'defaultDB', 30 | lists: { 31 | databaseList: ['defaultDB'], 32 | tableList: [], 33 | } 34 | }; 35 | 36 | componentDidMount() { 37 | ipcRenderer.send('return-db-list'); 38 | 39 | // Listening for returnedData from executing Query 40 | // Update state with new object (containing query data, query statistics, query schema 41 | // inside of state.queries array 42 | ipcRenderer.on('return-execute-query', (event: any, returnedData: any) => { 43 | // destructure from returnedData from backend 44 | const { queryString, queryData, queryStatistics, queryCurrentSchema, queryLabel } = returnedData; 45 | // create new query object with returnedData 46 | const newQuery = { 47 | queryString, 48 | queryData, 49 | queryStatistics, 50 | querySchema: queryCurrentSchema, 51 | queryLabel, 52 | } 53 | // create copy of current queries array 54 | let queries = this.state.queries.slice(); 55 | // push new query object into copy of queries array 56 | queries.push(newQuery) 57 | this.setState({ queries }) 58 | }); 59 | 60 | ipcRenderer.on('db-lists', (event: any, returnedLists: any) => { 61 | this.setState({ lists: returnedLists }) 62 | this.onClickTabItem(this.state.lists.databaseList[this.state.lists.databaseList.length - 1]) 63 | }) 64 | } 65 | 66 | onClickTabItem(tabName) { 67 | ipcRenderer.send('change-db', tabName); 68 | ipcRenderer.on('return-change-db', (event: any, db_name: string) => { 69 | this.setState({ currentSchema: tabName }); 70 | }); 71 | } 72 | 73 | render() { 74 | return ( 75 |
    76 |
    77 | 78 | 79 |
    80 | 81 |
    82 | ); 83 | } 84 | } 85 | 86 | export default MainPanel; 87 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/css/components.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); 3 | #query-window .text-field { 4 | width: 100%; 5 | height: 200px; 6 | } 7 | 8 | .label-field, 9 | .schema-label { 10 | font-family: "PT Mono", monospace; 11 | color: #6cbba9; 12 | margin-left: 8px; 13 | } 14 | 15 | #splash-menu button { 16 | border: 0.5px #444c50 solid; 17 | background-color: #596368; 18 | border-radius: 3px; 19 | padding: 5px; 20 | border: none; 21 | font-size: 1em; 22 | outline: none; 23 | } 24 | 25 | #splash-menu button:hover { 26 | background-color: #c6d2d5; 27 | } 28 | 29 | #query-panel button { 30 | border: 0.5px #444c50 solid; 31 | background-color: #596368; 32 | border-radius: 3px; 33 | padding: 5px; 34 | border: none; 35 | font-size: 0.8em; 36 | outline: none; 37 | } 38 | 39 | #query-panel button:hover { 40 | background-color: #c6d2d5; 41 | } 42 | 43 | #main-right button { 44 | border: 0.5px #444c50 solid; 45 | background-color: #596368; 46 | border-radius: 3px; 47 | border: none; 48 | outline: none; 49 | } 50 | 51 | #main-right button:hover { 52 | background-color: #6cbba9; 53 | } 54 | 55 | textarea { 56 | min-height: 300px; 57 | background-color: #444c50; 58 | margin: 10px 0; 59 | width: 90%; 60 | padding: 8px; 61 | outline: none; 62 | border: none; 63 | color: #6cbba9; 64 | font-family: "PT Mono", monospace; 65 | } 66 | 67 | table.scroll-box { 68 | border: 0.5px solid #444c50; 69 | background: none; 70 | overflow-y: scroll; 71 | padding: 5px; 72 | font-size: 1em; 73 | line-height: 1.5em; 74 | } 75 | 76 | tbody .top-row { 77 | border-bottom: 1px solid #444c50; 78 | } 79 | 80 | tbody .top-row td { 81 | font-weight: bold; 82 | } 83 | 84 | input { 85 | border-top-style: hidden; 86 | border-right-style: hidden; 87 | border-left-style: hidden; 88 | border-bottom-style: groove; 89 | background-color: #444c50; 90 | border: none; 91 | padding: 7px; 92 | min-width: 240px; 93 | outline: none; 94 | } 95 | 96 | input *:focus { 97 | outline: none; 98 | } 99 | 100 | #data-table { 101 | overflow: auto; 102 | height: 300px; 103 | } 104 | 105 | .query-data { 106 | width: 1000px; 107 | } 108 | 109 | #codemirror { 110 | padding: 15px 0; 111 | } 112 | 113 | #data-table::-webkit-scrollbar-track { 114 | background: #c6d2d5; 115 | } 116 | 117 | #data-table::-webkit-scrollbar-thumb { 118 | background-color: #444c50; 119 | } 120 | 121 | .input-schema { 122 | display: block; 123 | } 124 | 125 | /* width */ 126 | ::-webkit-scrollbar { 127 | width: 15px; 128 | } 129 | 130 | /* Track */ 131 | ::-webkit-scrollbar-track { 132 | box-shadow: inset 0px 0px 5px grey; 133 | border-radius: 10px; 134 | } 135 | 136 | /* Handle */ 137 | ::-webkit-scrollbar-thumb { 138 | background: #c6d2d5; 139 | border-radius: 15px; 140 | } 141 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/scss/layout.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | #splash-page { 4 | display: flex; 5 | flex: 1; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | height: 100vh; 10 | // overflow: hidden; 11 | color: $primary-color-darkmode; 12 | .splash-prompt { 13 | display: flex; 14 | flex-direction: column; 15 | margin-top: 30px; 16 | text-align: center; 17 | } 18 | .splash-buttons { 19 | display: flex; 20 | flex-direction: row; 21 | margin-top: 50px; 22 | button { 23 | border: 0.5px $border-darkmode solid; 24 | background-color: $button-darkmode; 25 | border-radius: 3px; 26 | padding: 10px; 27 | border: none; 28 | font-size: 1em; 29 | font-weight: bold; 30 | color: $background-darkmode; 31 | outline: none; 32 | margin: 0 5px; 33 | } 34 | button:hover { 35 | background-color: $mint-green; 36 | } 37 | } 38 | img { 39 | width: 300px; 40 | height: auto; 41 | margin-bottom: 30px; 42 | } 43 | .logo { 44 | background-image: url('../../images/logo_color.png'); 45 | width: 360px; 46 | height: 362px; 47 | } 48 | } 49 | 50 | #main-panel { 51 | display: flex; 52 | flex-direction: row; 53 | height: 100vh; 54 | overflow: hidden; 55 | background-image: url('../../images/logo_monochrome.png'); 56 | background-repeat: no-repeat; 57 | background-position-x: right; 58 | background-position-y: bottom; 59 | } 60 | 61 | #main-left { 62 | width: 34%; 63 | display: flex; 64 | flex-direction: column; 65 | padding: 15px; 66 | background-color: $background-darkmode-darker; 67 | } 68 | #history-panel { 69 | height: 250px; 70 | display: flex; 71 | flex-direction: column; 72 | } 73 | 74 | .history-container{ 75 | display: flex; 76 | flex-direction: column; 77 | height: 250px; 78 | overflow-y: auto; 79 | } 80 | 81 | #compare-panel { 82 | display: flex; 83 | flex-grow: 1; 84 | } 85 | 86 | #main-right { 87 | display: flex; 88 | flex-direction: column; 89 | flex-grow: 1; 90 | height: 100%; 91 | } 92 | 93 | #test-panels { 94 | display: flex; 95 | flex-direction: row; 96 | height: 100%; 97 | } 98 | 99 | #schema-left { 100 | display: flex; 101 | flex-direction: column; 102 | width: 50%; 103 | flex: 1; 104 | padding: 15px; 105 | border-right: 0.5px solid $border-darkmode; 106 | } 107 | #query-panel { 108 | display: flex; 109 | flex-direction: column; 110 | height: 50%; 111 | z-index: 1000; 112 | } 113 | #data-panel { 114 | display: flex; 115 | flex-direction: column; 116 | flex-grow: 1; 117 | } 118 | #schema-right { 119 | display: flex; 120 | flex-direction: column; 121 | width: 50%; 122 | padding: 15px; 123 | height: 100%; 124 | } 125 | 126 | #results-panel { 127 | display: flex; 128 | flex-grow: 1; 129 | flex-direction: column; 130 | } 131 | 132 | .results-container{ 133 | display:flex; 134 | flex-direction: column; 135 | height: 300px; 136 | overflow-y: auto; 137 | } 138 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/scss/components.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | #query-window { 4 | .text-field { 5 | width: 100%; 6 | height: 200px; 7 | } 8 | } 9 | 10 | .label-field, 11 | .schema-label { 12 | font-family: $font-input; 13 | color: $mint-green; 14 | margin-left: 8px; 15 | } 16 | 17 | #splash-menu { 18 | button { 19 | border: 0.5px $border-darkmode solid; 20 | background-color: $button-darkmode; 21 | border-radius: 3px; 22 | padding: 5px; 23 | border: none; 24 | font-size: 1em; 25 | outline: none; 26 | } 27 | button:hover { 28 | background-color: $primary-color-darkmode; 29 | } 30 | } 31 | 32 | #query-panel { 33 | button { 34 | border: 0.5px $border-darkmode solid; 35 | background-color: $button-darkmode; 36 | border-radius: 3px; 37 | padding: 5px; 38 | border: none; 39 | font-size: 0.8em; 40 | outline: none; 41 | } 42 | button:hover { 43 | background-color: $primary-color-darkmode; 44 | } 45 | } 46 | #main-right { 47 | button { 48 | border: 0.5px $border-darkmode solid; 49 | background-color: $button-darkmode; 50 | border-radius: 3px; 51 | border: none; 52 | // font-size: 0.8em; 53 | outline: none; 54 | } 55 | button:hover { 56 | background-color: $mint-green; 57 | } 58 | } 59 | textarea { 60 | min-height: 300px; 61 | background-color: $border-darkmode; 62 | margin: 10px 0; 63 | width: 90%; 64 | padding: 8px; 65 | outline: none; 66 | border: none; 67 | color: $mint-green; 68 | font-family: $font-input; 69 | } 70 | 71 | table.scroll-box { 72 | border: 0.5px solid $border-darkmode; 73 | background: none; 74 | overflow-y: scroll; 75 | padding: 5px; 76 | font-size: $default-text; 77 | line-height: 1.5em; 78 | } 79 | 80 | tbody { 81 | .top-row { 82 | border-bottom: 1px solid $border-darkmode; 83 | td { 84 | font-weight: bold; 85 | } 86 | } 87 | } 88 | 89 | input { 90 | border-top-style: hidden; 91 | border-right-style: hidden; 92 | border-left-style: hidden; 93 | border-bottom-style: groove; 94 | background-color: $border-darkmode; 95 | border: none; 96 | padding: 7px; 97 | min-width: 240px; 98 | outline: none; 99 | *:focus { 100 | outline: none; 101 | } 102 | } 103 | 104 | #data-table { 105 | overflow: auto; 106 | height: 300px; 107 | } 108 | 109 | .query-data { 110 | width: 1000px; 111 | } 112 | 113 | #codemirror { 114 | padding: 15px 0; 115 | } 116 | 117 | #data-table::-webkit-scrollbar-track { 118 | background: $primary-color-darkmode; 119 | } 120 | 121 | #data-table::-webkit-scrollbar-thumb { 122 | background-color: $border-darkmode; 123 | } 124 | 125 | 126 | 127 | .input-schema { 128 | display:block; 129 | } 130 | 131 | /* width */ 132 | ::-webkit-scrollbar { 133 | width: 15px; 134 | } 135 | 136 | /* Track */ 137 | ::-webkit-scrollbar-track { 138 | box-shadow: inset 0px 0px 5px grey; 139 | border-radius: 10px; 140 | } 141 | 142 | /* Handle */ 143 | ::-webkit-scrollbar-thumb { 144 | background: $primary-color-darkmode; 145 | border-radius: 15px; 146 | } 147 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/css/layout.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); 3 | #splash-page { 4 | display: flex; 5 | flex: 1; 6 | flex-direction: column; 7 | justify-content: center; 8 | align-items: center; 9 | height: 100vh; 10 | color: #c6d2d5; 11 | } 12 | 13 | #splash-page .splash-prompt { 14 | display: flex; 15 | flex-direction: column; 16 | margin-top: 30px; 17 | text-align: center; 18 | } 19 | 20 | #splash-page .splash-buttons { 21 | display: flex; 22 | flex-direction: row; 23 | margin-top: 50px; 24 | } 25 | 26 | #splash-page .splash-buttons button { 27 | border: 0.5px #444c50 solid; 28 | background-color: #596368; 29 | border-radius: 3px; 30 | padding: 10px; 31 | border: none; 32 | font-size: 1em; 33 | font-weight: bold; 34 | color: #2b2d35; 35 | outline: none; 36 | margin: 0 5px; 37 | } 38 | 39 | #splash-page .splash-buttons button:hover { 40 | background-color: #6cbba9; 41 | } 42 | 43 | #splash-page img { 44 | width: 300px; 45 | height: auto; 46 | margin-bottom: 30px; 47 | } 48 | 49 | #splash-page .logo { 50 | background-image: url("../../images/logo_color.png"); 51 | width: 360px; 52 | height: 362px; 53 | } 54 | 55 | #main-panel { 56 | display: flex; 57 | flex-direction: row; 58 | height: 100vh; 59 | overflow: hidden; 60 | background-image: url("../../images/logo_monochrome.png"); 61 | background-repeat: no-repeat; 62 | background-position-x: right; 63 | background-position-y: bottom; 64 | } 65 | 66 | #main-left { 67 | width: 34%; 68 | display: flex; 69 | flex-direction: column; 70 | padding: 15px; 71 | background-color: #292a30; 72 | } 73 | 74 | #history-panel { 75 | height: 250px; 76 | display: flex; 77 | flex-direction: column; 78 | } 79 | 80 | .history-container { 81 | display: flex; 82 | flex-direction: column; 83 | height: 250px; 84 | overflow-y: auto; 85 | } 86 | 87 | #compare-panel { 88 | display: flex; 89 | flex-grow: 1; 90 | } 91 | 92 | #main-right { 93 | display: flex; 94 | flex-direction: column; 95 | flex-grow: 1; 96 | height: 100%; 97 | } 98 | 99 | #test-panels { 100 | display: flex; 101 | flex-direction: row; 102 | height: 100%; 103 | } 104 | 105 | #schema-left { 106 | display: flex; 107 | flex-direction: column; 108 | width: 50%; 109 | flex: 1; 110 | padding: 15px; 111 | border-right: 0.5px solid #444c50; 112 | } 113 | 114 | #query-panel { 115 | display: flex; 116 | flex-direction: column; 117 | height: 50%; 118 | z-index: 1000; 119 | } 120 | 121 | #data-panel { 122 | display: flex; 123 | flex-direction: column; 124 | flex-grow: 1; 125 | } 126 | 127 | #schema-right { 128 | display: flex; 129 | flex-direction: column; 130 | width: 50%; 131 | padding: 15px; 132 | height: 100%; 133 | } 134 | 135 | #results-panel { 136 | display: flex; 137 | flex-grow: 1; 138 | flex-direction: column; 139 | } 140 | 141 | .results-container { 142 | display: flex; 143 | flex-direction: column; 144 | height: 300px; 145 | overflow-y: auto; 146 | } 147 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const { spawn } = require('child_process'); 4 | 5 | module.exports = { 6 | entry: "./frontend/index.tsx", 7 | mode: process.env.NODE_ENV, 8 | devtool: "eval-source-map", 9 | output: { 10 | filename: "bundle.js", 11 | path: path.resolve(__dirname, "dist"), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.s?css$/, 17 | use: [ 18 | { 19 | loader: "style-loader", // inject CSS to page 20 | }, 21 | { 22 | loader: "css-loader", // translates CSS into CommonJS modules 23 | }, 24 | { 25 | loader: "postcss-loader", // Run postcss actions 26 | options: { 27 | plugins() { 28 | // postcss plugins, can be exported to postcss.config.js 29 | return [require("autoprefixer")]; 30 | }, 31 | }, 32 | }, 33 | { 34 | loader: "sass-loader", // compiles Sass to CSS 35 | }, 36 | ], 37 | }, 38 | { 39 | test: /\.jsx?/, 40 | exclude: /node_modules/, 41 | use: { 42 | loader: "babel-loader", 43 | options: { 44 | presets: ["@babel/preset-env", "@babel/preset-react"], 45 | }, 46 | }, 47 | }, 48 | { 49 | test: /\.ts(x)?$/, 50 | exclude: /node_modules/, 51 | loader: "ts-loader", 52 | }, 53 | { 54 | test: /\.(jpg|jpeg|png|ttf|svg)$/, 55 | use: [ 56 | 'file-loader', 57 | { 58 | loader: 'image-webpack-loader', 59 | options: { 60 | mozjpeg: { 61 | quality: 10, 62 | }, 63 | }, 64 | }, 65 | ], 66 | exclude: /node_modules/, 67 | }, 68 | ], 69 | }, 70 | resolve: { 71 | // Enable importing JS / JSX files without specifying their extension 72 | modules: [path.resolve(__dirname, 'node_modules')], 73 | extensions: ['.js', '.jsx', '.json', '.scss', '.less', '.css', '.tsx', '.ts'], 74 | }, 75 | target: "electron-renderer", 76 | devServer: { 77 | contentBase: path.resolve(__dirname, "/tsCompiled/frontend"), 78 | host: "localhost", 79 | port: "8080", 80 | hot: true, 81 | compress: true, 82 | watchContentBase: true, 83 | watchOptions: { 84 | ignored: /node_modules/, 85 | }, 86 | before() { 87 | spawn('electron', ['.', 'dev'], { 88 | shell: true, 89 | env: process.env, 90 | stdio: 'inherit', 91 | }) 92 | .on('close', (code) => process.exit(0)) 93 | .on('error', (spawnError) => console.error(spawnError)); 94 | }, 95 | }, 96 | plugins: [ 97 | new HtmlWebpackPlugin({ 98 | filename: "index.html", 99 | title: "SeeQR", 100 | cspPlugin: { 101 | enabled: true, 102 | policy: { 103 | "base-uri": "'self'", 104 | "object-src": "'none'", 105 | "script-src": ["'self'"], 106 | "style-src": ["'self'"], 107 | }, 108 | hashEnabled: { 109 | "script-src": true, 110 | "style-src": true, 111 | }, 112 | nonceEnabled: { 113 | "script-src": true, 114 | "style-src": true, 115 | }, 116 | }, 117 | }), 118 | ], 119 | }; 120 | -------------------------------------------------------------------------------- /frontend/components/rightPanel/schemaChildren/Query.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const { ipcRenderer } = window.require('electron'); 4 | const { dialog } = require('electron').remote; 5 | 6 | // Codemirror configuration 7 | import 'codemirror/lib/codemirror.css'; // Styline 8 | import 'codemirror/mode/sql/sql'; // Language (Syntax Highlighting) 9 | import 'codemirror/theme/lesser-dark.css'; // Theme 10 | import CodeMirror from 'react-codemirror'; 11 | 12 | /************************************************************ 13 | *********************** TYPESCRIPT: TYPES *********************** 14 | ************************************************************/ 15 | 16 | type QueryProps = { currentSchema: string }; 17 | 18 | type state = { 19 | queryString: string; 20 | queryLabel: string; 21 | show: boolean; 22 | }; 23 | 24 | class Query extends Component { 25 | constructor(props: QueryProps) { 26 | super(props); 27 | this.handleQuerySubmit = this.handleQuerySubmit.bind(this); 28 | this.updateCode = this.updateCode.bind(this); 29 | // this.handleQueryPrevious = this.handleQueryPrevious.bind(this); 30 | // this.handleGenerateData = this.handleGenerateData.bind(this); 31 | } 32 | 33 | state: state = { 34 | queryString: '', 35 | queryLabel: '', 36 | show: false, 37 | }; 38 | 39 | // Updates state.queryString as user inputs query label 40 | handleLabelEntry(event: any) { 41 | this.setState({ queryLabel: event.target.value }); 42 | } 43 | 44 | // Updates state.queryString as user inputs query string 45 | updateCode(newQueryString: string) { 46 | this.setState({ 47 | queryString: newQueryString, 48 | }); 49 | } 50 | 51 | // Submits query to backend on 'execute-query' channel 52 | handleQuerySubmit(event: any) { 53 | event.preventDefault(); 54 | // if input fields for query label or query string are empty, then 55 | // send alert to input both fields 56 | if (!this.state.queryLabel || !this.state.queryString) { 57 | dialog.showErrorBox('Please enter a Label and a Query.', ''); 58 | } else { 59 | const queryAndSchema = { 60 | queryString: this.state.queryString, 61 | queryCurrentSchema: this.props.currentSchema, 62 | queryLabel: this.state.queryLabel, 63 | }; 64 | ipcRenderer.send('execute-query', queryAndSchema); 65 | } 66 | } 67 | 68 | // handleGenerateData(event: any) { 69 | // ipcRenderer.send('generate-data') 70 | // } 71 | 72 | render() { 73 | // Codemirror module configuration options 74 | var options = { 75 | lineNumbers: true, 76 | mode: 'sql', 77 | theme: 'lesser-dark', 78 | }; 79 | 80 | return ( 81 |
    82 |

    Query

    83 |
    84 | 85 | this.handleLabelEntry(e)} 90 | /> 91 |
    92 |
    93 | 94 | {/* */} 95 |
    96 | 100 |
    101 | 102 |
    103 |
    104 |

    *required

    105 |
    106 | {/* */} 107 |
    108 | ); 109 | } 110 | } 111 | 112 | export default Query; 113 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/scss/style.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './components.scss'; 3 | @import './layout.scss'; 4 | @import './modal.scss'; 5 | 6 | * { 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | body { 12 | background-color: $background-darkmode; 13 | font-family: $font-stack; 14 | font-weight: $p-weight; 15 | font-size: $default-text; 16 | line-height: 1/5; 17 | color: $primary-color-darkmode; 18 | height: 100%; 19 | } 20 | 21 | h3 { 22 | font-weight: bold; 23 | text-transform: uppercase; 24 | margin-bottom: 10px; 25 | } 26 | 27 | h4 { 28 | font-weight: $p-weight; 29 | font-size: 18px; 30 | } 31 | 32 | .compare-box { 33 | border: 0.5px solid #444c50; 34 | background: none; 35 | overflow: scroll; 36 | padding: 5px; 37 | font-size: 1em; 38 | line-height: 1.5em; 39 | } 40 | #compare-panel { 41 | display: -webkit-box; 42 | display: -ms-flexbox; 43 | display: flex; 44 | -webkit-box-orient: vertical; 45 | -webkit-box-direction: normal; 46 | -ms-flex-direction: column; 47 | flex-direction: column; 48 | margin-top: 2rem; 49 | } 50 | 51 | .compare-container { 52 | display: -webkit-box; 53 | display: -ms-flexbox; 54 | display: flex; 55 | -webkit-box-orient: vertical; 56 | -webkit-box-direction: normal; 57 | -ms-flex-direction: column; 58 | flex-direction: column; 59 | overflow-y: auto; 60 | height: 200px; 61 | } 62 | 63 | 64 | #add-query-button { 65 | width: 120px; 66 | margin-bottom: 15px; 67 | background-color: #596368; 68 | border-radius: 3px; 69 | padding: 5px; 70 | border: none; 71 | font-size: 0.8em; 72 | outline: none; 73 | } 74 | 75 | #add-query-button:hover { 76 | background-color: #c6d2d5; 77 | } 78 | 79 | .delete-query-button { 80 | width: 15px; 81 | background-color: transparent; 82 | font-size: 0.8em; 83 | outline: none; 84 | color: #c6d2d5; 85 | box-shadow: none; 86 | background-repeat: no-repeat; 87 | border: none; 88 | cursor: pointer; 89 | overflow: hidden; 90 | } 91 | 92 | .delete-query-button:hover { 93 | color: rgb(255, 0, 0); 94 | } 95 | 96 | .queryItem { 97 | background-color: #30353a; 98 | width: 100px; 99 | color: #c6d2d5; 100 | display: block; 101 | align-content: center; 102 | padding: 10px; 103 | text-decoration: none; 104 | font-family: 'PT Mono', monospace; 105 | } 106 | 107 | .queryItem:hover { 108 | background-color: #c6d2d5; 109 | color: #30353a; 110 | } 111 | 112 | .line-chart { 113 | margin-top: 2rem; 114 | } 115 | 116 | .bar-chart { 117 | margin-top: 3rem; 118 | height: 300px; 119 | display: block; 120 | } 121 | 122 | .tab-list { 123 | border-bottom: 2px solid #c6d2d5; 124 | // padding-left: 0 30 0 0; 125 | margin-right: 10px; 126 | display: flex; 127 | // justify-content: space-between; 128 | } 129 | 130 | .tab-list-item { 131 | display: inline-block; 132 | list-style: none; 133 | margin-bottom: -1px; 134 | padding: 0.5rem 0.75rem; 135 | font-weight: 500; 136 | letter-spacing: 0.5px; 137 | cursor: pointer; 138 | 139 | border: solid #ccc; 140 | border-width: 1px 1px 0 1px; 141 | border-radius: 15px 15px 0px 0px; 142 | } 143 | 144 | .tab-list-item:hover{ 145 | // background-color: #c6d2d5; 146 | color:#c6d2d5; 147 | } 148 | 149 | .tab-list-active { 150 | background-color:rgb(108, 187, 169); 151 | border: solid #ccc; 152 | border-width: 1px 1px 0 1px; 153 | border-radius: 15px 15px 0px 0px; 154 | font-weight: 500; 155 | color: $background-darkmode-darker; 156 | } 157 | .close-button { 158 | top: 20px; 159 | right: 30px; 160 | position: fixed; 161 | background-color: transparent; 162 | font-weight: bold; 163 | } 164 | 165 | #input-schema-button { 166 | // padding: px; 167 | width: 50px; 168 | margin-bottom: 5px; 169 | font-size: 2em; 170 | font-weight: bold; 171 | margin-left: 5px; 172 | } 173 | 174 | #input-schema-button:hover { 175 | // background-color: #c6d2d5; 176 | color:#c6d2d5; 177 | } 178 | 179 | button:hover { 180 | cursor: pointer; 181 | } 182 | 183 | // #generate-data-button { 184 | // margin-top: 30px; 185 | // } -------------------------------------------------------------------------------- /frontend/components/rightPanel/schemaChildren/Results.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Line, defaults } from "react-chartjs-2"; 3 | 4 | type ResultsProps = { 5 | queries: { 6 | queryString: string; 7 | queryData: {}[]; 8 | queryStatistics: any 9 | querySchema: string; 10 | queryLabel: string; 11 | }[]; 12 | }; 13 | 14 | defaults.global.defaultFontColor = 'rgb(198,210,213)'; 15 | 16 | export class Results extends Component { 17 | constructor(props: ResultsProps) { 18 | super(props); 19 | } 20 | renderTableData() { 21 | 22 | return this.props.queries.map((query, index) => { 23 | // destructure state from mainPanel, including destructuring object returned from Postgres 24 | const { queryString, queryData, queryStatistics, querySchema, queryLabel } = query; 25 | const { ['QUERY PLAN']: queryPlan } = queryStatistics[0]; 26 | const { 27 | Plan, 28 | ['Planning Time']: planningTime, 29 | ['Execution Time']: executionTime, 30 | } = queryPlan[0]; 31 | const { 32 | ['Node Type']: scanType, 33 | ['Actual Rows']: actualRows, 34 | ['Actual Startup Time']: actualStartupTime, 35 | ['Actual Total Time']: actualTotalTime, 36 | ['Actual Loops']: loops, 37 | } = Plan; 38 | const runtime = (planningTime + executionTime).toFixed(3); 39 | return ( 40 | 41 | {queryLabel} 42 | {queryString} 43 | {/* {scanType} */} 44 | {planningTime} 45 | {runtime} 46 | {/* {executionTime} 47 | {actualStartupTime} */} 48 | {/* {actualTotalTime} */} 49 | {/* {actualRows} */} 50 | {loops} 51 | {/* {'Notes'} */} 52 | 53 | ); 54 | }); 55 | } 56 | 57 | render() { 58 | const { queries } = this.props; 59 | const labelData = () => queries.map((query) => query.queryLabel); 60 | const runtimeData = () => queries.map( 61 | (query) => query.queryStatistics[0]["QUERY PLAN"][0]["Execution Time"] + query.queryStatistics[0]["QUERY PLAN"][0]["Planning Time"]); 62 | const data = { 63 | labels: labelData(), 64 | datasets: [ 65 | { 66 | label: 'Runtime', 67 | fill: false, 68 | lineTension: 0.5, 69 | backgroundColor: 'rgb(108, 187, 169)', 70 | borderColor: 'rgba(247,247,247,247)', 71 | borderWidth: 2, 72 | data: runtimeData(), 73 | } 74 | ] 75 | } 76 | 77 | // To display additional analytics, comment back in JSX elements in the return statement below. 78 | return ( 79 |
    80 |

    Results

    81 |
    82 | 83 | 84 | 85 | 86 | 87 | {/* */} 88 | 89 | 90 | {/* 91 | 92 | */} 93 | {/* */} 94 | {/* */} 95 | 96 | {/* */} 97 | 98 | {this.renderTableData()} 99 | 100 |
    {'Query Label'}{'Query'}{'Scan Type'}{'Planning Time'}{'Runtime (ms)'}{'Execution Time'}{'Time: First Line (ms)'}{'Time: All Lines (ms)'}{'Returned Rows'}{'Total Time (ms)'}{'Loops'}{'Notes'}
    101 |
    102 |
    103 | 117 |
    118 |
    119 | ); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /backend/main.ts: -------------------------------------------------------------------------------- 1 | // Import parts of electron to use 2 | import { app, BrowserWindow, ipcMain, Menu } from 'electron'; 3 | import { join } from 'path'; 4 | import { format } from 'url'; 5 | import './channels' // all channels live here 6 | 7 | const { exec } = require('child_process'); 8 | const appMenu = require('./mainMenu'); // use appMenu to add options in top menu bar of app 9 | const path = require('path'); 10 | 11 | /************************************************************ 12 | *********** PACKAGE ELECTRON APP FOR DEPLOYMENT *********** 13 | ************************************************************/ 14 | 15 | // Uncomment to package electron app. Ensures path is correct for MacOS within inherited shell. 16 | // const fixPath = require('fix-path'); 17 | // fixPath(); 18 | 19 | /************************************************************ 20 | ****************** CREATE & CLOSE WINDOW ****************** 21 | ************************************************************/ 22 | // Keep a global reference of the window objects, if you don't, the window will 23 | // be closed automatically when the JavaScript object is garbage collected. 24 | let mainWindow: any; 25 | 26 | let mainMenu = Menu.buildFromTemplate(require('./mainMenu')); 27 | // Keep a reference for dev mode 28 | let dev = false; 29 | if (process.env.NODE_ENV !== undefined && process.env.NODE_ENV === 'development') { 30 | dev = true; 31 | } 32 | 33 | // Create browser window 34 | function createWindow() { 35 | mainWindow = new BrowserWindow({ 36 | width: 1800, 37 | height: 1400, 38 | minWidth: 1500, 39 | minHeight: 1000, 40 | title: 'SeeQR', 41 | show: false, 42 | webPreferences: { nodeIntegration: true, enableRemoteModule: true }, 43 | icon: path.join(__dirname, '../../frontend/assets/images/seeqr_dock.png'), 44 | }); 45 | 46 | if (process.platform === 'darwin') { 47 | app.dock.setIcon(path.join(__dirname, '../../frontend/assets/images/seeqr_dock.png')); 48 | } 49 | 50 | // Load index.html of the app 51 | let indexPath; 52 | if (dev && process.argv.indexOf('--noDevServer') === -1) { 53 | indexPath = format({ 54 | protocol: 'http:', 55 | host: 'localhost:8080', 56 | pathname: 'index.html', 57 | slashes: true, 58 | }); 59 | mainWindow.webContents.openDevTools(); 60 | Menu.setApplicationMenu(mainMenu); 61 | } else { 62 | // In production mode, load the bundled version of index.html inside the dist folder. 63 | indexPath = format({ 64 | protocol: 'file:', 65 | pathname: join(__dirname, '../../dist', 'index.html'), 66 | slashes: true, 67 | }); 68 | } 69 | 70 | mainWindow.loadURL(indexPath); 71 | 72 | // Don't show until we are ready and loaded 73 | mainWindow.once('ready-to-show', (event) => { 74 | mainWindow.show(); 75 | const runDocker: string = `docker-compose up -d`; 76 | exec(runDocker, (error, stdout, stderr) => { 77 | if (error) { 78 | console.log(`error: ${error.message}`); 79 | return; 80 | } 81 | if (stderr) { 82 | console.log(`stderr: ${stderr}`); 83 | return; 84 | } 85 | console.log(`${stdout}`); 86 | }) 87 | }); 88 | 89 | // Emitted when the window is closed. 90 | mainWindow.on('closed', function () { 91 | // Stop and remove postgres-1 and busybox-1 Docker containers upon window exit. 92 | const pruneContainers: string = 'docker rm -f postgres-1 busybox-1'; 93 | const executeQuery = (str) => { 94 | exec(str, (error, stdout, stderr) => { 95 | if (error) { 96 | console.log(`error: ${error.message}`); 97 | return; 98 | } 99 | if (stderr) { 100 | console.log(`stderr: ${stderr}`); 101 | return; 102 | } 103 | console.log(`${stdout}`); 104 | }) 105 | }; 106 | executeQuery(pruneContainers); 107 | mainWindow = null; 108 | }); 109 | } 110 | 111 | // Invoke createWindow to create browser windows after Electron has been initialized. 112 | // Some APIs can only be used after this event occurs. 113 | app.on('ready', createWindow); 114 | 115 | // Quit when all windows are closed. 116 | app.on('window-all-closed', () => { 117 | // On macOS it is common for applications and their menu bar 118 | // to stay active until the user quits explicitly with Cmd + Q 119 | if (process.platform !== 'darwin') { 120 | app.quit(); 121 | } 122 | }); 123 | 124 | app.on('activate', () => { 125 | // On macOS it's common to re-create a window in the app when the 126 | // dock icon is clicked and there are no other windows open. 127 | if (mainWindow === null) { 128 | createWindow(); 129 | } 130 | }); -------------------------------------------------------------------------------- /frontend/components/rightPanel/schemaChildren/SchemaModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; 3 | import SchemaInput from './SchemaInput'; 4 | import GenerateData from './GenerateData'; 5 | 6 | const { dialog } = require('electron').remote; 7 | const { ipcRenderer } = window.require('electron'); 8 | 9 | type ClickEvent = React.MouseEvent; 10 | 11 | type SchemaModalProps = { 12 | show: boolean; 13 | showModal: any; 14 | onClose: any; 15 | }; 16 | 17 | type state = { 18 | schemaName: string; 19 | schemaFilePath: string; 20 | schemaEntry: string; 21 | redirect: boolean; 22 | }; 23 | 24 | class SchemaModal extends Component { 25 | constructor(props: SchemaModalProps) { 26 | super(props); 27 | this.handleSchemaSubmit = this.handleSchemaSubmit.bind(this); 28 | this.handleSchemaFilePath = this.handleSchemaFilePath.bind(this); 29 | this.handleSchemaEntry = this.handleSchemaEntry.bind(this); 30 | this.handleSchemaName = this.handleSchemaName.bind(this); 31 | 32 | // this.handleQueryPrevious = this.handleQueryPrevious.bind(this); 33 | // this.handleQuerySubmit = this.handleQuerySubmit.bind(this); 34 | } 35 | 36 | state: state = { 37 | schemaName: '', 38 | schemaFilePath: '', 39 | schemaEntry: '', 40 | redirect: false, 41 | }; 42 | 43 | 44 | // Set schema name 45 | handleSchemaName(event: any) { 46 | // convert input label name to lowercase only with no spacing to comply with db naming convention. 47 | const schemaNameInput = event.target.value; 48 | let dbSafeName = schemaNameInput.toLowerCase(); 49 | dbSafeName = dbSafeName.replace(/[^A-Z0-9]/gi, ''); 50 | this.setState({ schemaName: dbSafeName }); 51 | } 52 | 53 | // Load schema file path 54 | // When file path is uploaded, query entry is cleared. 55 | handleSchemaFilePath(event: ClickEvent) { 56 | event.preventDefault(); 57 | dialog 58 | .showOpenDialog({ 59 | properties: ['openFile'], 60 | filters: [{ name: 'Custom File Type', extensions: ['tar', 'sql'] }], 61 | message: 'Please upload .sql or .tar database file', 62 | }) 63 | .then((result: object) => { 64 | const filePath = result['filePaths']; 65 | this.setState({ schemaFilePath: filePath }); 66 | const schemaObj = { 67 | schemaName: this.state.schemaName, 68 | schemaFilePath: this.state.schemaFilePath, 69 | schemaEntry: '', 70 | }; 71 | ipcRenderer.send('input-schema', schemaObj); 72 | this.props.showModal(event); 73 | }) 74 | .catch((err: object) => { 75 | console.log('Error in handleSchemaFilePath method of SchemaModal.tsx.', err); 76 | }); 77 | } 78 | 79 | // When schema script is inserted, file path is cleared set dialog to warn user. 80 | handleSchemaEntry(event: any) { 81 | this.setState({ schemaEntry: event.target.value, schemaFilePath: '' }); 82 | // this.setState({ schemaFilePath: '' }); 83 | } 84 | 85 | handleSchemaSubmit(event: any) { 86 | event.preventDefault(); 87 | 88 | const schemaObj = { 89 | schemaName: this.state.schemaName, 90 | schemaFilePath: this.state.schemaFilePath, 91 | schemaEntry: this.state.schemaEntry, 92 | }; 93 | 94 | ipcRenderer.send('input-schema', schemaObj); 95 | } 96 | 97 | render() { 98 | if (this.props.show === false) { 99 | return null; 100 | } 101 | 102 | return ( 103 | 135 | ); 136 | } 137 | } 138 | 139 | export default SchemaModal; 140 | -------------------------------------------------------------------------------- /frontend/components/leftPanel/Compare.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import DropdownButton from 'react-bootstrap/DropdownButton'; 3 | import Dropdown from 'react-bootstrap/Dropdown'; 4 | import { Bar, defaults } from "react-chartjs-2"; 5 | 6 | defaults.global.defaultFontColor = 'rgb(198,210,213)'; 7 | 8 | type CompareProps = { 9 | queries: { 10 | queryString: string; 11 | queryData: {}[]; 12 | queryStatistics: any 13 | querySchema: string; 14 | queryLabel: string; 15 | }[]; 16 | currentSchema: string 17 | }; 18 | 19 | export const Compare = (props: CompareProps) => { 20 | let initial: any = { ...props, compareList: [] }; 21 | 22 | const [queryInfo, setCompare] = useState(initial); 23 | const addCompareQuery = (event) => { 24 | let compareList = queryInfo.compareList; 25 | props.queries.forEach((query) => { 26 | if (query.queryLabel === event.target.text) { 27 | compareList.push(query); 28 | } 29 | }); 30 | setCompare({ ...queryInfo, compareList }); 31 | } 32 | 33 | const deleteCompareQuery = (event) => { 34 | let compareList: any = queryInfo.compareList.filter( 35 | (query) => query.queryLabel !== event.target.id); 36 | setCompare({ ...queryInfo, compareList }); 37 | } 38 | 39 | const dropDownList = () => { 40 | return props.queries.map((query, index) => {query.queryLabel}); 41 | }; 42 | 43 | const renderCompare = () => { 44 | return queryInfo.compareList.map((query, index) => { 45 | const { queryString, queryData, queryStatistics, querySchema, queryLabel } = query; 46 | const { ['QUERY PLAN']: queryPlan } = queryStatistics[0]; 47 | const { 48 | Plan, 49 | ['Planning Time']: planningTime, 50 | ['Execution Time']: executionTime, 51 | } = queryPlan[0]; 52 | const { 53 | ['Node Type']: scanType, 54 | ['Actual Rows']: actualRows, 55 | ['Actual Startup Time']: actualStartupTime, 56 | ['Actual Total Time']: actualTotalTime, 57 | ['Actual Loops']: loops, 58 | } = Plan; 59 | const runtime = (planningTime + executionTime).toFixed(3); 60 | 61 | // To display additional analytics, comment back in JSX elements in the return statement below. 62 | return ( 63 | 64 | {queryLabel} 65 | {querySchema} 66 | {/* {queryString} */} 67 | {/* {scanType} */} 68 | {actualRows} 69 | {runtime} 70 | {/* {planningTime} 71 | {executionTime} 72 | {actualStartupTime} */} 73 | {actualTotalTime} 74 | {/* {loops} */} 75 | 76 | 77 | ); 78 | }); 79 | }; 80 | 81 | const { compareList } = queryInfo; 82 | const labelData = () => compareList.map((query) => query.queryLabel); 83 | const runtimeData = () => compareList.map( 84 | (query) => query.queryStatistics[0]["QUERY PLAN"][0]["Execution Time"] + query.queryStatistics[0]["QUERY PLAN"][0]["Planning Time"]); 85 | const data = { 86 | labels: labelData(), 87 | datasets: [ 88 | { 89 | label: 'Runtime', 90 | backgroundColor: 'rgb(108, 187, 169)', 91 | borderColor: 'rgba(247,247,247,247)', 92 | borderWidth: 2, 93 | data: runtimeData(), 94 | } 95 | ] 96 | }; 97 | 98 | return ( 99 |
    100 |

    Comparisons

    101 | 102 | {dropDownList()} 103 | 104 |
    105 | 106 | 107 | 108 | 109 | 110 | 111 | {/* */} 112 | {/* */} 113 | 114 | 115 | {/* */} 116 | {/* */} 117 | 118 | {renderCompare()} 119 | 120 |
    {'Query Label'}{'Schema'}{'Total Rows'}{'Scan Type'}{'Query'}{'Runtime (ms)'}{'Total Time'}{'Returned Rows'}{'Loops'}
    121 |
    122 |
    123 | 137 |
    138 |
    139 | ); 140 | }; 141 | 142 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | /* Basic Options */ 5 | // "incremental": true, /* Enable incremental compilation */ 6 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 8 | // "lib": [], /* Specify library files to be included in the compilation. */ 9 | "allowJs": true /* Allow javascript files to be compiled. */, 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | "sourceMap": true /* Generates corresponding '.map' file. */, 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./tsCompiled" /* Redirect output structure to the directory. */, 17 | "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | "noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */, 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | /* Additional Checks */ 35 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 36 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 37 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 38 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 39 | /* Module Resolution Options */ 40 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 41 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 42 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 43 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 44 | // "typeRoots": [], /* List of folders to include type definitions from. */ 45 | // "types": [], /* Type declaration files to be included in compilation. */ 46 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 47 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 48 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 49 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 50 | /* Source Map Options */ 51 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 52 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 53 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 54 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 55 | /* Experimental Options */ 56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 58 | /* Advanced Options */ 59 | "skipLibCheck": true /* Skip type checking of declaration files. */, 60 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 61 | "resolveJsonModule": true /* Include modules imported with '.json' extension. Requires TypeScript version 2.9 or later. */ 62 | }, 63 | "exclude": [ 64 | "node_modules", 65 | "dist", 66 | "tsCompiled" 67 | ] 68 | } -------------------------------------------------------------------------------- /backend/channels.ts: -------------------------------------------------------------------------------- 1 | // Import parts of electron to use 2 | import { ipcMain } from 'electron'; 3 | 4 | const { exec } = require('child_process'); 5 | const db = require('./models'); 6 | 7 | /************************************************************ 8 | *********************** IPC CHANNELS *********************** 9 | ************************************************************/ 10 | 11 | // Global variable to store list of databases and tables to provide to frontend upon refreshing view. 12 | let listObj; 13 | 14 | ipcMain.on('return-db-list', (event, args) => { 15 | db.getLists().then(data => event.sender.send('db-lists', data)); 16 | }); 17 | 18 | // Listen for skip button on Splash page. 19 | ipcMain.on('skip-file-upload', (event) => { }); 20 | 21 | // Listen for database changes sent from the renderer upon changing tabs. 22 | ipcMain.on('change-db', (event, dbName) => { 23 | db.changeDB(dbName); 24 | event.sender.send('return-change-db', dbName); 25 | }); 26 | 27 | // Generate CLI commands to be executed in child process. 28 | const createDBFunc = (name) => { 29 | return `docker exec postgres-1 psql -h localhost -p 5432 -U postgres -c "CREATE DATABASE ${name}"` 30 | } 31 | 32 | const importFileFunc = (file) => { 33 | return `docker cp ${file} postgres-1:/data_dump`; 34 | } 35 | 36 | const runSQLFunc = (file) => { 37 | return `docker exec postgres-1 psql -U postgres -d ${file} -f /data_dump`; 38 | } 39 | 40 | const runTARFunc = (file) => { 41 | return `docker exec postgres-1 pg_restore -U postgres -d ${file} /data_dump`; 42 | } 43 | 44 | // Function to execute commands in the child process. 45 | const execute = (str: string, nextStep: any) => { 46 | exec(str, (error, stdout, stderr) => { 47 | if (error) { 48 | console.log(`error: ${error.message}`); 49 | return; 50 | } 51 | if (stderr) { 52 | console.log(`stderr: ${stderr}`); 53 | return; 54 | } 55 | console.log(`${stdout}`); 56 | if (nextStep) nextStep(); 57 | }); 58 | }; 59 | 60 | // Listen for file upload. Create an instance of database from pre-made .tar or .sql file. 61 | ipcMain.on('upload-file', (event, filePath: string) => { 62 | let dbName: string; 63 | if (process.platform === 'darwin') { 64 | dbName = filePath[0].slice(filePath[0].lastIndexOf('/') + 1, filePath[0].lastIndexOf('.')); 65 | } else { 66 | dbName = filePath[0].slice(filePath[0].lastIndexOf('\\') + 1, filePath[0].lastIndexOf('.')); 67 | } 68 | 69 | const createDB: string = createDBFunc(dbName); 70 | const importFile: string = importFileFunc(filePath); 71 | const runSQL: string = runSQLFunc(dbName); 72 | const runTAR: string = runTARFunc(dbName); 73 | const extension: string = filePath[0].slice(filePath[0].lastIndexOf('.')); 74 | 75 | // SEQUENCE OF EXECUTING COMMANDS 76 | // Steps are in reverse order because each step is a callback function that requires the following step to be defined. 77 | 78 | // Step 4: Changes the pg URI the newly created database, queries new database, then sends list of tables and list of databases to frontend. 79 | async function sendLists() { 80 | listObj = await db.getLists(); 81 | event.sender.send('db-lists', listObj); 82 | // Send schema name back to frontend, so frontend can load tab name. 83 | event.sender.send('return-schema-name', dbName) 84 | }; 85 | 86 | // Step 3 : Given the file path extension, run the appropriate command in postgres to populate db. 87 | const step3 = () => { 88 | let runCmd: string = ''; 89 | if (extension === '.sql') runCmd = runSQL; 90 | else if (extension === '.tar') runCmd = runTAR; 91 | execute(runCmd, sendLists); 92 | }; 93 | 94 | // Step 2 : Import database file from file path into docker container 95 | const step2 = () => execute(importFile, step3); 96 | 97 | // Step 1 : Create empty db 98 | if (extension === '.sql' || extension === '.tar') execute(createDB, step2); 99 | else console.log('INVALID FILE TYPE: Please use .tar or .sql extensions.'); 100 | }); 101 | 102 | interface SchemaType { 103 | schemaName: string; 104 | schemaFilePath: string; 105 | schemaEntry: string; 106 | } 107 | 108 | // Listen for schema edits (via file upload OR via CodeMirror inout) from schemaModal. Create an instance of database from pre-made .tar or .sql file. 109 | ipcMain.on('input-schema', (event, data: SchemaType) => { 110 | const { schemaName: dbName, schemaFilePath: filePath, schemaEntry } = data; 111 | 112 | // Using RegEx to remove line breaks to ensure data.schemaEntry is being run as one large string 113 | // so that schemaEntry string will work for Windows computers. 114 | let trimSchemaEntry = schemaEntry.replace(/[\n\r]/g, "").trim(); 115 | 116 | const createDB: string = createDBFunc(dbName); 117 | const importFile: string = importFileFunc(filePath); 118 | const runSQL: string = runSQLFunc(dbName); 119 | const runTAR: string = runTARFunc(dbName); 120 | 121 | const runScript: string = `docker exec postgres-1 psql -U postgres -d ${dbName} -c "${trimSchemaEntry}"`; 122 | let extension: string = ''; 123 | if (filePath.length > 0) { 124 | extension = filePath[0].slice(filePath[0].lastIndexOf('.')); 125 | } 126 | 127 | // SEQUENCE OF EXECUTING COMMANDS 128 | // Steps are in reverse order because each step is a callback function that requires the following step to be defined. 129 | 130 | // Step 4: Changes the pg URI to look to the newly created database and queries all the tables in that database and sends it to frontend. 131 | async function sendLists() { 132 | listObj = await db.getLists(); 133 | event.sender.send('db-lists', listObj); 134 | }; 135 | 136 | // Step 3 : Given the file path extension, run the appropriate command in postgres to build the db 137 | const step3 = () => { 138 | let runCmd: string = ''; 139 | if (extension === '.sql') runCmd = runSQL; 140 | else if (extension === '.tar') runCmd = runTAR; 141 | else runCmd = runScript; 142 | execute(runCmd, sendLists); 143 | }; 144 | 145 | // Step 2 : Import database file from file path into docker container 146 | const step2 = () => execute(importFile, step3); 147 | 148 | // Step 1 : Create empty db 149 | if (extension === '.sql' || extension === '.tar') execute(createDB, step2); 150 | // if data is inputted as text 151 | else execute(createDB, step3); 152 | }); 153 | 154 | interface QueryType { 155 | queryCurrentSchema: string; 156 | queryString: string; 157 | queryLabel: string; 158 | queryData: string; 159 | queryStatistics: string; 160 | } 161 | 162 | // Listen for queries being sent from renderer 163 | ipcMain.on('execute-query', (event, data: QueryType) => { 164 | // destructure object from frontend 165 | const { queryString, queryCurrentSchema, queryLabel } = data; 166 | 167 | // initialize object to store all data to send to frontend 168 | let frontendData = { 169 | queryString, 170 | queryCurrentSchema, 171 | queryLabel, 172 | queryData: '', 173 | queryStatistics: '', 174 | lists: {}, 175 | }; 176 | 177 | // Run select * from actors; 178 | db.query(queryString) 179 | .then((queryData) => { 180 | frontendData.queryData = queryData.rows; 181 | 182 | // Run EXPLAIN (FORMAT JSON, ANALYZE) 183 | db.query('EXPLAIN (FORMAT JSON, ANALYZE) ' + queryString).then((queryStats) => { 184 | frontendData.queryStatistics = queryStats.rows; 185 | 186 | (async function getListAsync() { 187 | listObj = await db.getLists(); 188 | frontendData.lists = listObj; 189 | event.sender.send('db-lists', listObj) 190 | event.sender.send('return-execute-query', frontendData); 191 | })(); 192 | }); 193 | }) 194 | .catch((error: string) => { 195 | console.log('ERROR in execute-query channel in main.ts', error); 196 | }); 197 | }); 198 | 199 | module.exports; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/oslabs-beta/SeeQR) 6 | ![Release: 1.0](https://img.shields.io/badge/Release-1.0-red) 7 | ![License: MIT](https://img.shields.io/badge/License-MIT-orange.svg) 8 | ![Contributions Welcome](https://img.shields.io/badge/Contributions-welcome-blue.svg) 9 | [![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Ftheseeqr)](https://twitter.com/theseeqr) 10 | [![Github stars](https://img.shields.io/github/stars/oslabs-beta/SeeQR?style=social)](https://github.com/oslabs-beta/SeeQR) 11 | [theSeeQR.io](http://www.theseeqr.io) 12 | 13 |

    SeeQR: A database analytic tool that compares the efficiency of different schemas and queries on a granular level to make better informed architectural decisions regarding SQL databases at various scales.

    14 | 15 |
    16 | 17 | ## Table of Contents 18 | 19 | - [Beta Phase](#beta-phase) 20 | - [Getting Started](#getting-started) 21 | - [Built With](#built-with) 22 | - [Interface & Features](#interface-&-features) 23 | - Schema upload methods 24 | - Query input 25 | - Data 26 | - History 27 | - Results 28 | - Compare 29 | - Dummy data generation 30 | - Visualized Analytics 31 | - [Application Architecture and Logic](#application-architecture-and-logic) 32 | - [Testing](#testing) 33 | - [Core Team](#core-team) 34 | 35 | ## Beta Phase 36 | 37 | SeeQR is still in BETA. Additional features, extensions, and improvements will continue to be introduced. If you encounter any issues with the application, please report them in the issues tab or submit a PR. Thank you for your interest! 38 | 39 | ## Getting Started 40 | 41 | To get started on contributing to this project: 42 | 43 | 1. Download and Install [Docker Desktop](https://www.docker.com/get-started) 44 | 2. Fork or clone this repository 45 | 3. Npm install 46 | 1. Run `npm install` for application-specific dependencies. 47 | 2. Run global install for: `'cross-env'`, `'webpack'`, `'webpack-dev-server'`, `'electron'`, and `'typescript'`. 48 | 4. Enable sass compiling to css directory 49 | 50 | ```json 51 | "liveSassCompile.settings.formats": [ 52 | { 53 | "format": "expanded", 54 | "savePath": "/frontend/assets/stylesheets/css" 55 | } 56 | ], 57 | "liveSassCompile.settings.generateMap": false, 58 | ``` 59 | 60 | 5. To run application during development 61 | 1. `npm run dev` to launch Electron application window and webpack-dev-server. 62 | 2. `npm run resetContainer` to reset the container and clear pre-existing SeeQR databases. If error “can’t find postgres-1” is encountered, it is simply an indication that the container is already pruned. 63 | 64 | ## Built With 65 | 66 | - [Electron](https://www.electronjs.org/docs) 67 | - [React](https://reactjs.org/) 68 | - [React-Hooks](https://reactjs.org/docs/hooks-intro.html) 69 | - [Typescript](https://www.typescriptlang.org/) 70 | - [Docker](https://www.docker.com/get-started) 71 | - [Docker-Compose](https://docs.docker.com/compose/) 72 | - [PostgreSQL](https://www.postgresql.org/) 73 | - [Chart.js](https://github.com/chartjs) 74 | - [Faker.js](https://github.com/Marak/faker.js) 75 | - [CodeMirror](https://codemirror.net/) 76 | 77 | ## Interface & Features 78 |
    79 |

    The whole interface in a nutshell

    80 |
    81 | 82 | - Schema 83 | - Upon application launch, upload `.sql` or `.tar` file when prompted by splash page, or hit cancel. 84 | - The uploaded `.sql` or `.tar` file becomes the active database. 85 | - To input new schemas, toggle the “Input Schema” button. Upload a .sql or .tar file or directly input schema code. Remember to provide the schema with a unique label, as it will be assigned to the name property of the newly spun up database connected to the schema. 86 | 87 | - Query input 88 | 89 | - The center panel is where the query input text field is located, utilizing CodeMirror for SQL styling. 90 | - Provide a unique and concise label for the query as its shorthand identifier in later comparisons against other queries. 91 | - Toggle the submit button in the bottom left to send the query to the selected database. 92 |

    93 |
    94 |

    95 | 96 |
    97 |

    98 | 99 | - Data 100 | 101 | - The data table displays data returned by the inputted query. 102 |
    103 |

    104 |
    105 | 106 | - Input Schema and Tabs 107 | - New schemas can be uploaded into the application by clicking the "+" button above the main panel in the form of a ```.sql``` or a ```.tar``` file, or the schema script itself. 108 | - Newly uploaded schemas are displayed as tabs, which can be activated to run tests against during application session. These schemas (and the databases they're connected to) persist beyond the application session. 109 |
    110 | 111 |
    112 | 113 | - History 114 | 115 | - The history table shows the latest queries the user submitted irrespective of the database. 116 | - The history table also displays the total rows returned by the query and the total query execution time. 117 |
    118 | 119 |
    120 | 121 | - Results 122 | 123 | - The results table displays the scan type, runtime, and the amount of loops the query had to perform in addition to the analytics data available on the history table. 124 | - The results table is schema-specific, showing only query results from the active schema. 125 |
    126 | 127 |
    128 | 129 | - Compare 130 | 131 | - The comparison table is flexible to the user’s preferences. 132 | - The user selects which queries they want to compare side by side from the ‘Add Query Data’ drop down. 133 | - They can add and remove queries as they see fit. 134 | 135 |
    136 | 137 |
    138 | 139 | - Visualized Analytics 140 | 141 | - Upon each query execution, query runtime displays under the "Query Label vs Query Runtime" graph. Graph automatically interpolates as results enumerate. 142 | - User may toggle on specific query analytics results with the Comparisons panel to compare query performances. 143 | 144 |
    145 | 146 | 147 |
    148 | 149 | ## Application Architecture and Logic 150 | 151 | Containerization
    152 | SeeQR streamlines the process of instantiating postgres databases by leveraging Docker to containerize an image of postgres. This means instances of databases are automatically created every time new schema data is uploaded or inputted via the SeeQR GUI. Electron communicates with the instantiated database’s URI’s by taking advantage of the `'pg'` npm package. 153 | 154 | Cross-schema Comparisons
    155 | One of the key features of SeeQR is to compare the efficiency of executing user-inputted queries against different schemas. This allows customization of table scale, relationship, type, and the queries themselves within the context of each schema. This flexibility affords the user granular adjustments for testing every desired scenario. Please refer to “Interface & Functionality” for more details on execution. 156 | 157 | Database:Schema 1:1 Architecture
    158 | While it is feasible for a database to house multiple schemas, SeeQR’s default architecture for database:schema relations is 1:1. For every schema inputted, a new database is generated to hold that schema. This architecture serves the application’s central purpose: testing — by enabling the capacity to individually scale data connected to each schema, generating analytics at any user-specified conditions. 159 | 160 | Session-based Result Caching
    161 | The outcome results from each query, both retrieved data and analytics, are stored in the application’s state, which can be viewed and compared in table and visualizer formats. Note that these results’ persistence is session-based and will be cleared upon quitting the application. 162 | 163 | ## Core Team 164 | 165 | 166 | 167 | 171 | 175 | 179 | 183 | 187 |
    168 |
    169 | Catherine Chiu 170 |
    172 |
    173 | Serena Kuo 174 |
    176 |
    177 | Frank Norton 178 |
    180 |
    181 | Mercer Stronck 182 |
    184 |
    185 | Muhammad Trad 186 |
    188 | -------------------------------------------------------------------------------- /frontend/assets/stylesheets/css/style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); 3 | @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 4 | @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); 5 | @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 6 | @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); 7 | @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); 8 | @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); 9 | #query-window .text-field { 10 | width: 100%; 11 | height: 200px; 12 | } 13 | 14 | .label-field, 15 | .schema-label { 16 | font-family: "PT Mono", monospace; 17 | color: #6cbba9; 18 | margin-left: 8px; 19 | } 20 | 21 | #splash-menu button { 22 | border: 0.5px #444c50 solid; 23 | background-color: #596368; 24 | border-radius: 3px; 25 | padding: 5px; 26 | border: none; 27 | font-size: 1em; 28 | outline: none; 29 | } 30 | 31 | #splash-menu button:hover { 32 | background-color: #c6d2d5; 33 | } 34 | 35 | #query-panel button { 36 | border: 0.5px #444c50 solid; 37 | background-color: #596368; 38 | border-radius: 3px; 39 | padding: 5px; 40 | border: none; 41 | font-size: 0.8em; 42 | outline: none; 43 | } 44 | 45 | #query-panel button:hover { 46 | background-color: #c6d2d5; 47 | } 48 | 49 | #main-right button { 50 | border: 0.5px #444c50 solid; 51 | background-color: #596368; 52 | border-radius: 3px; 53 | border: none; 54 | outline: none; 55 | } 56 | 57 | #main-right button:hover { 58 | background-color: #6cbba9; 59 | } 60 | 61 | textarea { 62 | min-height: 300px; 63 | background-color: #444c50; 64 | margin: 10px 0; 65 | width: 90%; 66 | padding: 8px; 67 | outline: none; 68 | border: none; 69 | color: #6cbba9; 70 | font-family: "PT Mono", monospace; 71 | } 72 | 73 | table.scroll-box { 74 | border: 0.5px solid #444c50; 75 | background: none; 76 | overflow-y: scroll; 77 | padding: 5px; 78 | font-size: 1em; 79 | line-height: 1.5em; 80 | } 81 | 82 | tbody .top-row { 83 | border-bottom: 1px solid #444c50; 84 | } 85 | 86 | tbody .top-row td { 87 | font-weight: bold; 88 | } 89 | 90 | input { 91 | border-top-style: hidden; 92 | border-right-style: hidden; 93 | border-left-style: hidden; 94 | border-bottom-style: groove; 95 | background-color: #444c50; 96 | border: none; 97 | padding: 7px; 98 | min-width: 240px; 99 | outline: none; 100 | } 101 | 102 | input *:focus { 103 | outline: none; 104 | } 105 | 106 | #data-table { 107 | overflow: auto; 108 | height: 300px; 109 | } 110 | 111 | .query-data { 112 | width: 1000px; 113 | } 114 | 115 | #codemirror { 116 | padding: 15px 0; 117 | } 118 | 119 | #data-table::-webkit-scrollbar-track { 120 | background: #c6d2d5; 121 | } 122 | 123 | #data-table::-webkit-scrollbar-thumb { 124 | background-color: #444c50; 125 | } 126 | 127 | .input-schema { 128 | display: block; 129 | } 130 | 131 | /* width */ 132 | ::-webkit-scrollbar { 133 | width: 15px; 134 | } 135 | 136 | /* Track */ 137 | ::-webkit-scrollbar-track { 138 | box-shadow: inset 0px 0px 5px grey; 139 | border-radius: 10px; 140 | } 141 | 142 | /* Handle */ 143 | ::-webkit-scrollbar-thumb { 144 | background: #c6d2d5; 145 | border-radius: 15px; 146 | } 147 | 148 | #splash-page { 149 | display: flex; 150 | flex: 1; 151 | flex-direction: column; 152 | justify-content: center; 153 | align-items: center; 154 | height: 100vh; 155 | color: #c6d2d5; 156 | } 157 | 158 | #splash-page .splash-prompt { 159 | display: flex; 160 | flex-direction: column; 161 | margin-top: 30px; 162 | text-align: center; 163 | } 164 | 165 | #splash-page .splash-buttons { 166 | display: flex; 167 | flex-direction: row; 168 | margin-top: 50px; 169 | } 170 | 171 | #splash-page .splash-buttons button { 172 | border: 0.5px #444c50 solid; 173 | background-color: #596368; 174 | border-radius: 3px; 175 | padding: 10px; 176 | border: none; 177 | font-size: 1em; 178 | font-weight: bold; 179 | color: #2b2d35; 180 | outline: none; 181 | margin: 0 5px; 182 | } 183 | 184 | #splash-page .splash-buttons button:hover { 185 | background-color: #6cbba9; 186 | } 187 | 188 | #splash-page img { 189 | width: 300px; 190 | height: auto; 191 | margin-bottom: 30px; 192 | } 193 | 194 | #splash-page .logo { 195 | background-image: url("../../images/logo_color.png"); 196 | width: 360px; 197 | height: 362px; 198 | } 199 | 200 | #main-panel { 201 | display: flex; 202 | flex-direction: row; 203 | height: 100vh; 204 | overflow: hidden; 205 | background-image: url("../../images/logo_monochrome.png"); 206 | background-repeat: no-repeat; 207 | background-position-x: right; 208 | background-position-y: bottom; 209 | } 210 | 211 | #main-left { 212 | width: 34%; 213 | display: flex; 214 | flex-direction: column; 215 | padding: 15px; 216 | background-color: #292a30; 217 | } 218 | 219 | #history-panel { 220 | height: 250px; 221 | display: flex; 222 | flex-direction: column; 223 | } 224 | 225 | .history-container { 226 | display: flex; 227 | flex-direction: column; 228 | height: 250px; 229 | overflow-y: auto; 230 | } 231 | 232 | #compare-panel { 233 | display: flex; 234 | flex-grow: 1; 235 | } 236 | 237 | #main-right { 238 | display: flex; 239 | flex-direction: column; 240 | flex-grow: 1; 241 | height: 100%; 242 | } 243 | 244 | #test-panels { 245 | display: flex; 246 | flex-direction: row; 247 | height: 100%; 248 | } 249 | 250 | #schema-left { 251 | display: flex; 252 | flex-direction: column; 253 | width: 50%; 254 | flex: 1; 255 | padding: 15px; 256 | border-right: 0.5px solid #444c50; 257 | } 258 | 259 | #query-panel { 260 | display: flex; 261 | flex-direction: column; 262 | height: 50%; 263 | z-index: 1000; 264 | } 265 | 266 | #data-panel { 267 | display: flex; 268 | flex-direction: column; 269 | flex-grow: 1; 270 | } 271 | 272 | #schema-right { 273 | display: flex; 274 | flex-direction: column; 275 | width: 50%; 276 | padding: 15px; 277 | height: 100%; 278 | } 279 | 280 | #results-panel { 281 | display: flex; 282 | flex-grow: 1; 283 | flex-direction: column; 284 | } 285 | 286 | .results-container { 287 | display: flex; 288 | flex-direction: column; 289 | height: 300px; 290 | overflow-y: auto; 291 | } 292 | 293 | .modal { 294 | width: 615px; 295 | height: 700px; 296 | background-color: #30353a; 297 | border: 0.5px solid #1a1a1a; 298 | transition: 1.1s ease-out; 299 | box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); 300 | filter: blur(0); 301 | transform: scale(1); 302 | opacity: 1; 303 | visibility: visible; 304 | padding: 40px; 305 | z-index: 1010; 306 | position: fixed; 307 | top: 100px; 308 | } 309 | 310 | .modal button { 311 | padding: 10px; 312 | margin: 10px 0; 313 | } 314 | 315 | .modal.off { 316 | opacity: 0; 317 | visibility: hidden; 318 | filter: blur(8px); 319 | transform: scale(0.33); 320 | box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); 321 | } 322 | 323 | .modal h2 { 324 | border-bottom: 1px solid #ccc; 325 | padding: 1rem; 326 | margin: 0; 327 | } 328 | 329 | .modal .content { 330 | padding: 1rem; 331 | } 332 | 333 | .modal input { 334 | margin: 10px 0; 335 | display: block; 336 | color: #6cbba9; 337 | font-family: "PT Mono", monospace; 338 | } 339 | 340 | .schema-text-field { 341 | height: 300px; 342 | width: 600px; 343 | } 344 | 345 | .modal-buttons { 346 | display: flex; 347 | flex-direction: row; 348 | } 349 | 350 | .modal-buttons .input-button { 351 | margin-left: 15px; 352 | } 353 | 354 | * { 355 | margin: 0; 356 | padding: 0; 357 | } 358 | 359 | body { 360 | background-color: #2b2d35; 361 | font-family: "PT Sans", sans-serif; 362 | font-weight: 100; 363 | font-size: 1em; 364 | line-height: 1/5; 365 | color: #c6d2d5; 366 | height: 100%; 367 | } 368 | 369 | h3 { 370 | font-weight: bold; 371 | text-transform: uppercase; 372 | margin-bottom: 10px; 373 | } 374 | 375 | h4 { 376 | font-weight: 100; 377 | font-size: 18px; 378 | } 379 | 380 | .compare-box { 381 | border: 0.5px solid #444c50; 382 | background: none; 383 | overflow: scroll; 384 | padding: 5px; 385 | font-size: 1em; 386 | line-height: 1.5em; 387 | } 388 | 389 | #compare-panel { 390 | display: flex; 391 | flex-direction: column; 392 | margin-top: 2rem; 393 | } 394 | 395 | .compare-container { 396 | display: flex; 397 | flex-direction: column; 398 | overflow-y: auto; 399 | height: 200px; 400 | } 401 | 402 | #add-query-button { 403 | width: 120px; 404 | margin-bottom: 15px; 405 | background-color: #596368; 406 | border-radius: 3px; 407 | padding: 5px; 408 | border: none; 409 | font-size: 0.8em; 410 | outline: none; 411 | } 412 | 413 | #add-query-button:hover { 414 | background-color: #c6d2d5; 415 | } 416 | 417 | .delete-query-button { 418 | width: 15px; 419 | background-color: transparent; 420 | font-size: 0.8em; 421 | outline: none; 422 | color: #c6d2d5; 423 | box-shadow: none; 424 | background-repeat: no-repeat; 425 | border: none; 426 | cursor: pointer; 427 | overflow: hidden; 428 | } 429 | 430 | .delete-query-button:hover { 431 | color: red; 432 | } 433 | 434 | .queryItem { 435 | background-color: #30353a; 436 | width: 100px; 437 | color: #c6d2d5; 438 | display: block; 439 | align-content: center; 440 | padding: 10px; 441 | text-decoration: none; 442 | font-family: 'PT Mono', monospace; 443 | } 444 | 445 | .queryItem:hover { 446 | background-color: #c6d2d5; 447 | color: #30353a; 448 | } 449 | 450 | .line-chart { 451 | margin-top: 2rem; 452 | } 453 | 454 | .bar-chart { 455 | margin-top: 3rem; 456 | height: 300px; 457 | display: block; 458 | } 459 | 460 | .tab-list { 461 | border-bottom: 2px solid #c6d2d5; 462 | margin-right: 10px; 463 | display: flex; 464 | } 465 | 466 | .tab-list-item { 467 | display: inline-block; 468 | list-style: none; 469 | margin-bottom: -1px; 470 | padding: 0.5rem 0.75rem; 471 | font-weight: 500; 472 | letter-spacing: 0.5px; 473 | cursor: pointer; 474 | border: solid #ccc; 475 | border-width: 1px 1px 0 1px; 476 | border-radius: 15px 15px 0px 0px; 477 | } 478 | 479 | .tab-list-item:hover { 480 | color: #c6d2d5; 481 | } 482 | 483 | .tab-list-active { 484 | background-color: #6cbba9; 485 | border: solid #ccc; 486 | border-width: 1px 1px 0 1px; 487 | border-radius: 15px 15px 0px 0px; 488 | font-weight: 500; 489 | color: #292a30; 490 | } 491 | 492 | .close-button { 493 | top: 20px; 494 | right: 30px; 495 | position: fixed; 496 | background-color: transparent; 497 | font-weight: bold; 498 | } 499 | 500 | #input-schema-button { 501 | width: 50px; 502 | margin-bottom: 5px; 503 | font-size: 2em; 504 | font-weight: bold; 505 | margin-left: 5px; 506 | } 507 | 508 | #input-schema-button:hover { 509 | color: #c6d2d5; 510 | } 511 | 512 | button:hover { 513 | cursor: pointer; 514 | } 515 | --------------------------------------------------------------------------------