diff --git a/.gitignore b/.gitignore index 2943acad..0c770853 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /node_modules -/.vscode /dist /tsCompiled */.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..586b1602 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "liveSassCompile.settings.formats": [ + { + "format": "expanded", + "savePath": "/frontend/assets/stylesheets/css", + "extensionName": ".css" + } + ], + "liveSassCompile.settings.generateMap": false +} \ No newline at end of file diff --git a/README.md b/README.md index 42c45e9e..a891a5ec 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,7 @@ To get started on contributing to this project: 3. Npm install 1. Run `npm install` for application-specific dependencies. 2. Run global install for: `'cross-env'`, `'webpack'`, `'webpack-dev-server'`, `'electron'`, and `'typescript'`. -4. Enable sass compiling to css directory - -```json -"liveSassCompile.settings.formats": [ - { - "format": "expanded", - "savePath": "/frontend/assets/stylesheets/css" - } - ], -"liveSassCompile.settings.generateMap": false, -``` - +4. Install [Live Sass Compile](https://github.com/ritwickdey/vscode-live-sass-compiler) VSCode extension (settings are configured in the .vscode file in this repo), or set up your preferred Sass compiler 5. To run application during development 1. `npm run dev` to launch Electron application window and webpack-dev-server. 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. @@ -76,7 +65,7 @@ To get started on contributing to this project: ## Interface & Features
-

The whole interface in a nutshell

+

The whole interface in a nutshell

- Schema @@ -87,12 +76,11 @@ To get started on contributing to this project: - Query input - The center panel is where the query input text field is located, utilizing CodeMirror for SQL styling. - - Provide a unique and concise label for the query as its shorthand identifier in later comparisons against other queries. + - Users have option to execute a tracked or untracked query—simply check the box and provide a label to identify the query in later comparisons against other queries. - Toggle the submit button in the bottom left to send the query to the selected database.

-

- +


@@ -100,14 +88,25 @@ To get started on contributing to this project: - The data table displays data returned by the inputted query.
-

+

- Input Schema and Tabs - - 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. - - 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. + - 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. + - Users can also make a copy of an existing schema, with or without data included. + - Newly uploaded schemas are displayed as tabs, which can be activated to run tests against during application session. +
+ +
+ +- Generate Dummy Data + - Users can generate dummy data to fill in a selected scheama's tables—currently supported data types are: + - INT, SMALLINT, and BIGINT + - please fill in + - Dummy data is foreign-key complaint. + - please fill in the details
- +
- History @@ -115,17 +114,9 @@ To get started on contributing to this project: - The history table shows the latest queries the user submitted irrespective of the database. - The history table also displays the total rows returned by the query and the total query execution time.
- +
-- Results - - - 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. - - The results table is schema-specific, showing only query results from the active schema. -
- -
- - Compare - The comparison table is flexible to the user’s preferences. @@ -133,17 +124,17 @@ To get started on contributing to this project: - They can add and remove queries as they see fit.
- +
- Visualized Analytics - Upon each query execution, query runtime displays under the "Query Label vs Query Runtime" graph. Graph automatically interpolates as results enumerate. - User may toggle on specific query analytics results with the Comparisons panel to compare query performances. + - Graph will be organized on x-axis by label, and colored by schema.
- - +
## Application Architecture and Logic @@ -185,3 +176,22 @@ The outcome results from each query, both retrieved data and analytics, are stor Muhammad Trad + + + + + + +
+
+Justin Dury-Agri +
+
+Casey Escovedo +
+
+Sam Frakes +
+
+Casey Walker +
diff --git a/__tests__/leftPanelTests/compareTest.jsx b/__tests__/leftPanelTests/compareTest.jsx new file mode 100644 index 00000000..217e85c2 --- /dev/null +++ b/__tests__/leftPanelTests/compareTest.jsx @@ -0,0 +1,32 @@ +import * as React from "react"; +import { Compare } from "../../frontend/components/leftPanel/Compare"; +import { shallow } from "enzyme"; + +describe ("Comparison feature tests", () => { + // wrapper will be assigned the evaluation of the shallow render + let wrapper; + + const props = { + queries: [], + currentSchema: '', + } + // shallow render the component before running tests + beforeAll(() => { + wrapper = shallow() + }) + + it('Should render a div', () => { + expect(wrapper.type()).toEqual('div'); + }) + + it('Should render correct h3 element', () => { + expect(wrapper.containsMatchingElement( +

Comparisons

)).toBeTruthy(); + }) + + it('Should render query label', () => { + expect(wrapper.containsMatchingElement( + {'Query Label'})).toBeTruthy(); + }) + +}) \ No newline at end of file diff --git a/__tests__/leftPanelTests/historyTest.jsx b/__tests__/leftPanelTests/historyTest.jsx new file mode 100644 index 00000000..25b1a735 --- /dev/null +++ b/__tests__/leftPanelTests/historyTest.jsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { History } from "../../frontend/components/leftPanel/History"; +import { shallow } from "enzyme"; + +describe ("History feature tests", () => { + // wrapper will be assigned the evaluation of the shallow render + let wrapper; + + const props = { + queries: [], + currentSchema: '', + + } + // shallow render the component before running tests + beforeAll(() => { + wrapper = shallow() + }) + + it('Should render a div', () => { + expect(wrapper.type()).toEqual('div'); + }) + + it('Should render correct h3 element', () => { + expect(wrapper.containsMatchingElement( +

History

)).toBeTruthy(); + }) + + it('Should render query label', () => { + expect(wrapper.containsMatchingElement( + {'Query Label'})).toBeTruthy(); + }) + +}) \ No newline at end of file diff --git a/__tests__/rightPanelTests/schemaChildrenTests/dataChildrenTests/dataTableTest.jsx b/__tests__/rightPanelTests/schemaChildrenTests/dataChildrenTests/dataTableTest.jsx new file mode 100644 index 00000000..a008fedb --- /dev/null +++ b/__tests__/rightPanelTests/schemaChildrenTests/dataChildrenTests/dataTableTest.jsx @@ -0,0 +1,33 @@ +import * as React from 'react'; +import { mount, shallow } from 'enzyme'; +import { Table } from '../../../../frontend/components/rightPanel/schemaChildren/dataChildren/DataTable'; + +const dummyRowData = [{"header0":"input0", "header1":1}] + +const dummyTableProps = { + queries: [{ + queryString: "string", + queryData: dummyRowData, + queryStatistics: 7, + querySchema: "string", + queryLabel: "string" + }] +}; + +describe('Testing the data table', () => { + let wrapper; + beforeAll(() => { + wrapper = mount(); + }) + + it('should render Table headers', () => { + expect(wrapper.find('#dataTableHead').type()).toBe('thead'); + expect(wrapper.find('#dataTableHead').childAt(0).childAt(0).text()).toBe('HEADER0'); + expect(wrapper.find('#dataTableHead').childAt(0).childAt(1).text()).toBe('HEADER1'); + }) + + it('should render data Table body element', () => { + expect(wrapper.find('#dataTableBody').type()).toBe('tbody'); + }) +}) + diff --git a/__tests__/rightPanelTests/schemaChildrenTests/dataTest.jsx b/__tests__/rightPanelTests/schemaChildrenTests/dataTest.jsx new file mode 100644 index 00000000..78053df3 --- /dev/null +++ b/__tests__/rightPanelTests/schemaChildrenTests/dataTest.jsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { Data } from '../../../frontend/components/rightPanel/schemaChildren/Data'; + + +const dummyTableProps = { + queries: [{ + queryString: "string", + queryData: [{}], + queryStatistics: 7, + querySchema: "string", + queryLabel: "string" + }] +}; + +describe ("Data tests", () => { + const { queries } = dummyTableProps; + + // shallow render the component before running tests + let wrapper; + beforeAll(() => { + wrapper = shallow() + }) + + it('Should render a div', () => { + expect(wrapper.type()).toEqual('div'); + }) + + it('Should render h3 tag', () => { + expect(wrapper.containsMatchingElement( +

Data Table

)).toBeTruthy(); + }) + + it('Should render div to contain the data table', () => { + expect(wrapper.find('#data-table').type()).toBe('div'); + }) +}) diff --git a/__tests__/rightPanelTests/tabsChildrenTests/tabTest.jsx b/__tests__/rightPanelTests/tabsChildrenTests/tabTest.jsx new file mode 100644 index 00000000..5fcdbdc9 --- /dev/null +++ b/__tests__/rightPanelTests/tabsChildrenTests/tabTest.jsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { Tab } from '../../../frontend/components/rightPanel/tabsChildren/Tab'; + +const dummyTabProps = { + onClickTabItem: 'string', + currentSchema: "string", + label: "string", +}; + +describe ("Tab tests", () => { + // shallow render the component before running tests + let wrapper; + beforeAll(() => { + wrapper = shallow() + }) + + it('Should render a list item', () => { + expect(wrapper.type()).toEqual('li'); + }) +}) diff --git a/__tests__/setupTests.js b/__tests__/setupTests.js new file mode 100644 index 00000000..2f1d4217 --- /dev/null +++ b/__tests__/setupTests.js @@ -0,0 +1,10 @@ +import { configure } from "enzyme"; +import React16Adapter from "enzyme-adapter-react-16"; + +configure({ adapter: new React16Adapter() }); + +describe('Setup', () => { + it('should run before all tests', () => { + expect(true).toBe(true); + }) +}) \ No newline at end of file diff --git a/__tests__/splashTest.jsx b/__tests__/splashTest.jsx new file mode 100644 index 00000000..5bb89568 --- /dev/null +++ b/__tests__/splashTest.jsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import { Splash } from "../frontend/components/Splash"; +import { shallow } from "enzyme"; + +describe ("Splash page tests", () => { + // mock functions to pass to handlers + const mockFileClick = jest.fn(() => console.log("click")); + const mockSkipClick = jest.fn(() => console.log("skipClick")); + // props to be passed to the shallow render of the component + const props = { + openSplash: true, + handleSkipClick: mockSkipClick, + handleFileClick: mockFileClick + }; + + let wrapper; + // shallow render the component before running tests + beforeAll(() => { + wrapper = shallow() + }); + + it('should find the correct elements by id', () => { + expect(wrapper.find('#skip_button').type()).toBe('button'); + expect(wrapper.find('#yes_button').type()).toBe('button'); + }); + + it('The functions passed down should be invoked on click', () => { + // testing the skip button + wrapper.find('#skip_button').simulate('click'); + expect(mockSkipClick).toHaveBeenCalled(); + // testing the import button + wrapper.find('#yes_button').simulate('click'); + expect(mockFileClick).toHaveBeenCalled(); + }); +}); diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 00000000..810987f5 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,12 @@ +module.exports = { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + ], +}; \ No newline at end of file diff --git a/backend/DummyD/dummyDataMain.ts b/backend/DummyD/dummyDataMain.ts new file mode 100644 index 00000000..0e897b3f --- /dev/null +++ b/backend/DummyD/dummyDataMain.ts @@ -0,0 +1,273 @@ +import faker from "faker"; +import execute from "../channels"; +const db = require('../models'); + +///////////////////////////////////////////////////////////////////// +/* THIS FILE CONTAINS THE ALGORITHMS THAT GENERATE DUMMY DATA */ +/* */ +/* - The functions below are called in channels.ts */ +/* - This process runs for each table where data is requested */ +/* - generateDummyData creates dummy data values in a table matrix */ +/* - This matrix is passed to writeCSV file function, which writes */ +/* a file to the postgres-1 container */ +///////////////////////////////////////////////////////////////////// + +let keyObject: any; + +//this object is generated by a method in models.ts +type schemaLayout = { + tableNames: string[]; + tables: any; +} + +//this object is created on the front end in DummyDataModal +type dummyDataRequest = { + schemaName: string; + dummyData: {}; +} + +//helper function to generate random numbers that will ultimately represent a random date +const getRandomInt = (min, max) => { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min) + min); //The maximum is exclusive and the minimum is inclusive +} + +// this function generates data for a column +// column data coming in is an object of the form +// ex: { +// 'data_type': 'integer'; +// 'character_maximum_length': null +// } +const generateDataByType = (columnObj) => { + //faker.js method to generate data by type + switch (columnObj.dataInfo.data_type) { + case 'smallint': + return faker.random.number({min: -32768, max: 32767}); + case 'integer': + return faker.random.number({min: -2147483648, max: 2147483647}); + case 'bigint': + return faker.random.number({min: -9223372036854775808, max: 9223372036854775807}); + case 'character varying': + if (columnObj.dataInfo.character_maximum_length) { + return faker.lorem.character(Math.floor(Math.random() * columnObj.dataInfo.character_maximum_length)); + } + else return faker.lorem.word(); + case 'date': + // generating a random date between 1500 and 2020 + let result: string = ''; + let year: string = getRandomInt(1500, 2020).toString(); + let month: string = getRandomInt(1, 13).toString(); + if (month.length === 1) month = '0' + month; + let day: string = getRandomInt(1, 29).toString(); + if (day.length === 1) day = '0' + day; + result += year + '-' + month + '-' + day; + return result; + default: + console.log('Error generating dummy data by type') + } +}; + +// initialize a counter to make sure we are only adding back constraints once we've dropped and re-added columns +let count: number = 0; + +module.exports = { + writeCSVFile: (tableObject, schemaLayout, keyObject, dummyDataRequest, event: any) => { + // extracting variables + const tableCount: number = Object.keys(dummyDataRequest.dummyData).length; + const tableName: string = tableObject.tableName; + const tableMatrix: any = tableObject.data; + const schemaName: string = dummyDataRequest.schemaName; + + // mapping column headers from getColumnObjects in models.ts to columnNames + const columnArray: string[] = schemaLayout.tables[tableName].map(columnObj => columnObj.columnName); + + // transpose the table-matrix to orient it as a table + const table: any = []; + let row: any = []; + for(let i = 0; i < tableMatrix[0].length; i++) { + for(let j = 0; j < tableMatrix.length; j++) { + row.push(tableMatrix[j][i]); + } + //join each subarray (which correspond to rows in our table) with a comma + const rowString = row.join(','); + table.push(rowString); //'1, luke, etc' + row = []; + } + + // Step 3 - this step adds back the PK constraints that we took off prior to copying the dummy data into the DB (using the db that is imported from models.ts) + const step3 = () => { + count += 1; + let checkLast: number = tableCount - count; + if (checkLast === 0) { + db.addPrimaryKeyConstraints(keyObject, dummyDataRequest) + .then(() => { + db.addForeignKeyConstraints(keyObject, dummyDataRequest) + .then(() => { + event.sender.send('async-complete'); + count = 0; + }) + .catch((err) => { + console.log(err); + count = 0; + }); + }) + .catch((err) => { + console.log(err); + count = 0; + }); + } + else return; + } + + // Step 2 - using the postgres COPY command, this step copies the contents of the csv file in the container file system into the appropriate postgres DB + const step2 = () => { + let queryString: string = `COPY ${tableName} FROM '/${tableName}.csv' WITH CSV HEADER;`; + // run the query in the container using a docker command + execute(`docker exec postgres-1 psql -U postgres -d ${schemaName} -c "${queryString}" `, step3); + } + + let csvString: string; + //join tableMatrix with a line break (different on mac and windows because of line breaks in the bash CLI) + if (process.platform === 'win32') { + const tableDataString: string = table.join(`' >> ${tableName}.csv; echo '`); + const columnString: string = columnArray.join(','); + csvString = columnString.concat(`' > ${tableName}.csv; echo '`).concat(tableDataString); + execute(`docker exec postgres-1 bash -c "echo '${csvString}' >> ${tableName}.csv;"`, step2); + } + else { + // we know we are not on Windows, thank god! + const tableDataString: string = table.join('\n'); + const columnString: string = columnArray.join(','); + csvString = columnString.concat('\n').concat(tableDataString); + + // split csv string into an array of csv strings that each are of length 100,000 characters or less + + // create upperLimit variable, which represents that max amount of character a bash shell command can handle + let upperLimit: number; + upperLimit = 100000; + // create stringCount variable that is equal to csvString divided by upper limit rounded up + let stringCount: number = Math.ceil(csvString.length / upperLimit); + // create csvArray that will hold our final csv strings + let csvArray: string[] = []; + + let startIndex: number; + let endIndex: number; + // iterate over i from 0 to less than stringCount, each iteration pushing slices of original csvString into an array + for (let i = 0; i < stringCount; i += 1) { + startIndex = upperLimit * i; + endIndex = startIndex + upperLimit; + // if on final iteration, only give startIndex to slice operator to grab characters until the end of csvString + if (i === stringCount - 1) csvArray.push(csvString.slice(startIndex)); + else csvArray.push(csvString.slice(startIndex, endIndex)); + } + let index: number = 0 + // Step 1 - this writes a csv file to the postgres-1 file system, which contains all of the dummy data that will be copied into its corresponding postgres DB + const step1 = () => { + // NOTE: in order to rewrite the csv files in the container file system, we must use echo with a single angle bracket on the first element of csvArray AND then move on directly to step2 (and then also reset index) + + // if our csvArray contains only one element + if (csvArray.length === 1) { + execute(`docker exec postgres-1 bash -c "echo '${csvArray[index]}' > ${tableName}.csv;"`, step2); + index = 0; + } + // otherwise if we are working with the first element in csvArray + else if (index === 0) { + execute(`docker exec postgres-1 bash -c "echo -n '${csvArray[index]}' > ${tableName}.csv;"`, step1); + index += 1; + } + // if working with last csvArray element, execute docker command but pass in step2 as second argument + else if (index === (csvArray.length - 1)) { + // console.log('FINAL STEP 1: ', csvArray[index]); + execute(`docker exec postgres-1 bash -c "echo '${csvArray[index]}' >> ${tableName}.csv;"`, step2); + index = 0; + } + // otherwise we know we are not working with the first OR the last element in csvArray, so execute docker command but pass in a recursive call to our step one function and then immediately increment our index variable + else { + // console.log('STEP 1: ', index, csvArray[index]); + execute(`docker exec postgres-1 bash -c "echo -n '${csvArray[index]}' >> ${tableName}.csv;"`, step1); + index += 1; + } + } + step1(); + } + }, + + + //maps table names from schemaLayout to sql files + generateDummyData: (schemaLayout, dummyDataRequest, keyObject) => { + const returnArray: any = []; + + //iterate over schemaLayout.tableNames array + for (const tableName of schemaLayout.tableNames) { + const tableMatrix: any = []; + //if matching key exists in dummyDataRequest.dummyData + if (dummyDataRequest.dummyData[tableName]) { + //declare empty columnData array for tableMatrix + let columnData: any = []; + //declare an entry variable to capture the entry we will push to column data + let entry: any; + + //iterate over columnArray (i.e. an array of the column names for the table) + let columnArray: string[] = schemaLayout.tables[tableName].map(columnObj => columnObj.columnName) + for (let i = 0; i < columnArray.length; i++) { + // declare a variable j (to be used in while loops below), set equal to zero + let j: number = 0; + // if there are either PK or FK columns on this table, enter this logic + if (keyObject[tableName]) { + // if this is a PK column, add numbers into column 0 to n-1 (ordered) + if (keyObject[tableName].primaryKeyColumns[columnArray[i]]) { + //while i < reqeusted number of rows + while (j < dummyDataRequest.dummyData[tableName]) { + //push into columnData + columnData.push(j); + // increment j + j += 1; + } + } + // if this is a FK column, add random number between 0 and n-1 (inclusive) into column (unordered) + else if (keyObject[tableName].foreignKeyColumns[columnArray[i]]) { + //while j < reqeusted number of rows + while (j < dummyDataRequest.dummyData[tableName]) { + //generate an entry + entry = Math.floor(Math.random() * (dummyDataRequest.dummyData[tableName])); + //push into columnData + columnData.push(entry); + j += 1; + } + } + // otherwise, we'll just add data by the type to which the column is constrained + else { + while (j < dummyDataRequest.dummyData[tableName]) { + //generate an entry + entry = generateDataByType(schemaLayout.tables[tableName][i]); + //push into columnData + columnData.push(entry); + j += 1; + }; + } + } + // otherwise, we'll just add data by the type to which the column is constrained + else { + while (j < dummyDataRequest.dummyData[tableName]) { + //generate an entry + entry = generateDataByType(schemaLayout.tables[tableName][i]); + //push into columnData + columnData.push(entry); + j += 1; + }; + } + + //push columnData array into tableMatrix + tableMatrix.push(columnData); + //reset columnData array for next column + columnData = []; + }; + // only push something to the array if data was asked for for the specific table + returnArray.push({tableName, data: tableMatrix}); + }; + }; + // then return the returnArray + return returnArray; + } +} \ No newline at end of file diff --git a/backend/DummyD/foreign_key_info.ts b/backend/DummyD/foreign_key_info.ts new file mode 100644 index 00000000..3c80592b --- /dev/null +++ b/backend/DummyD/foreign_key_info.ts @@ -0,0 +1,49 @@ +module.exports= { + // This query lists each table that has a foreign key, the name of the table that key points to, and the name of the column at which the foreign key constraint resides + getForeignKeys: + `select kcu.table_name as foreign_table, + rel_kcu.table_name as primary_table, + kcu.column_name as fk_column + from information_schema.table_constraints tco + join information_schema.key_column_usage kcu + on tco.constraint_name = kcu.constraint_name + join information_schema.referential_constraints rco + on tco.constraint_name = rco.constraint_name + join information_schema.key_column_usage rel_kcu + on rco.unique_constraint_name = rel_kcu.constraint_name + where tco.constraint_type = 'FOREIGN KEY' + order by kcu.table_schema, + kcu.table_name, + kcu.ordinal_position;`, + + // This query lists each table and the column name at which there is a primary key + getPrimaryKeys: + `select kcu.table_name as table_name, + kcu.column_name as pk_column + from information_schema.key_column_usage as kcu + join information_schema.table_constraints as tco + on tco.constraint_name = kcu.constraint_name + where tco.constraint_type = 'PRIMARY KEY' + order by kcu.table_name;`, +} + + + +// structure of the key object for generating key compliant data +// const KeyObject = { +// // people: +// Table_1: { +// primaryKeyColumns: { +// // id: true +// _id: true +// } +// foreignKeyColumns: { +// // species_id: n where n is the number of rows asked for in the primary table the key points to +// foreignKeyColumnName_1: numOfRows, +// foreignKeyColumnName_2: numOfRows +// } +// } +// . +// . +// . +// } \ No newline at end of file diff --git a/backend/channels.ts b/backend/channels.ts index 567ae7da..a8c6c09b 100644 --- a/backend/channels.ts +++ b/backend/channels.ts @@ -1,64 +1,79 @@ // Import parts of electron to use -import { ipcMain } from 'electron'; +import { dialog, ipcMain } from 'electron'; +const { generateDummyData, writeCSVFile } = require('./DummyD/dummyDataMain'); const { exec } = require('child_process'); const db = require('./models'); /************************************************************ - *********************** IPC CHANNELS *********************** + *********************** Helper functions ******************* ************************************************************/ -// Global variable to store list of databases and tables to provide to frontend upon refreshing view. -let listObj; - -ipcMain.on('return-db-list', (event, args) => { - db.getLists().then(data => event.sender.send('db-lists', data)); -}); - -// Listen for skip button on Splash page. -ipcMain.on('skip-file-upload', (event) => { }); - -// Listen for database changes sent from the renderer upon changing tabs. -ipcMain.on('change-db', (event, dbName) => { - db.changeDB(dbName); - event.sender.send('return-change-db', dbName); -}); - // Generate CLI commands to be executed in child process. const createDBFunc = (name) => { return `docker exec postgres-1 psql -h localhost -p 5432 -U postgres -c "CREATE DATABASE ${name}"` } - const importFileFunc = (file) => { return `docker cp ${file} postgres-1:/data_dump`; } - -const runSQLFunc = (file) => { - return `docker exec postgres-1 psql -U postgres -d ${file} -f /data_dump`; +const runSQLFunc = (dbName) => { + return `docker exec postgres-1 psql -U postgres -d ${dbName} -f /data_dump`; } - -const runTARFunc = (file) => { - return `docker exec postgres-1 pg_restore -U postgres -d ${file} /data_dump`; +const runTARFunc = (dbName) => { + return `docker exec postgres-1 pg_restore -U postgres -d ${dbName} /data_dump`; +} +const runFullCopyFunc = (dbCopyName) => { + return `docker exec postgres-1 pg_dump -U postgres ${dbCopyName} -f /data_dump`; +} +const runHollowCopyFunc = (dbCopyName) => { + return `docker exec postgres-1 pg_dump -s -U postgres ${dbCopyName} -f /data_dump`; } // Function to execute commands in the child process. const execute = (str: string, nextStep: any) => { exec(str, (error, stdout, stderr) => { if (error) { + //this shows the console error in an error message on the frontend + dialog.showErrorBox(`${error.message}`, ''); console.log(`error: ${error.message}`); return; } if (stderr) { + //this shows the console error in an error message on the frontend + dialog.showErrorBox(`${stderr}`, ''); console.log(`stderr: ${stderr}`); return; } - console.log(`${stdout}`); + // console.log('exec func', `${stdout}`); if (nextStep) nextStep(); }); }; +/************************************************************ + *********************** IPC CHANNELS *********************** + ************************************************************/ + +// Global variable to store list of databases and tables to provide to frontend upon refreshing view. +let listObj: any; + +ipcMain.on('return-db-list', (event, args) => { + db.getLists().then(data => event.sender.send('db-lists', data)); +}); + +// Listen for skip button on Splash page. +ipcMain.on('skip-file-upload', (event) => { }); + +// Listen for database changes sent from the renderer upon changing tabs. +ipcMain.on('change-db', (event, dbName) => { + db.changeDB(dbName) +}); + // Listen for file upload. Create an instance of database from pre-made .tar or .sql file. ipcMain.on('upload-file', (event, filePath: string) => { + + // send notice to the frontend that async process has begun + event.sender.send('async-started'); + let dbName: string; if (process.platform === 'darwin') { dbName = filePath[0].slice(filePath[0].lastIndexOf('/') + 1, filePath[0].lastIndexOf('.')); @@ -75,80 +90,131 @@ ipcMain.on('upload-file', (event, filePath: string) => { // SEQUENCE OF EXECUTING COMMANDS // Steps are in reverse order because each step is a callback function that requires the following step to be defined. - // Step 4: Changes the pg URI the newly created database, queries new database, then sends list of tables and list of databases to frontend. + // Step 5: Changes the pg URI the newly created database, queries new database, then sends list of tables and list of databases to frontend. async function sendLists() { listObj = await db.getLists(); + console.log('channels: ', listObj); event.sender.send('db-lists', listObj); // Send schema name back to frontend, so frontend can load tab name. - event.sender.send('return-schema-name', dbName) + event.sender.send('return-schema-name', dbName); + // tell the front end to switch tabs to the newly created database + event.sender.send('switch-to-new', null); + // notify frontend that async process has been completed + event.sender.send('async-complete'); }; - // Step 3 : Given the file path extension, run the appropriate command in postgres to populate db. - const step3 = () => { + // Step 4: Given the file path extension, run the appropriate command in postgres to populate db. + const step4 = () => { let runCmd: string = ''; if (extension === '.sql') runCmd = runSQL; else if (extension === '.tar') runCmd = runTAR; execute(runCmd, sendLists); }; - // Step 2 : Import database file from file path into docker container - const step2 = () => execute(importFile, step3); + // Step 3: Import database file from file path into docker container + const step3 = () => execute(importFile, step4); - // Step 1 : Create empty db + // Step 2: Change curent URI to match newly created DB + const step2 = () => { + db.changeDB(dbName); + return step3(); + } + + // Step 1: Create empty db if (extension === '.sql' || extension === '.tar') execute(createDB, step2); else console.log('INVALID FILE TYPE: Please use .tar or .sql extensions.'); }); interface SchemaType { schemaName: string; - schemaFilePath: string; + schemaFilePath: string[]; schemaEntry: string; + dbCopyName: string; + copy: boolean; } -// 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. +// The following function creates an instance of database from pre-made .tar or .sql file. +// OR +// Listens for and handles DB copying events ipcMain.on('input-schema', (event, data: SchemaType) => { - const { schemaName: dbName, schemaFilePath: filePath, schemaEntry } = data; - // Using RegEx to remove line breaks to ensure data.schemaEntry is being run as one large string - // so that schemaEntry string will work for Windows computers. - let trimSchemaEntry = schemaEntry.replace(/[\n\r]/g, "").trim(); + // send notice to the frontend that async process has begun + event.sender.send('async-started'); + + const { schemaName: dbName, dbCopyName, copy } = data; + let { schemaFilePath: filePath } = data; + // generate strings that are fed into execute functions later const createDB: string = createDBFunc(dbName); const importFile: string = importFileFunc(filePath); const runSQL: string = runSQLFunc(dbName); const runTAR: string = runTARFunc(dbName); + const runFullCopy: string = runFullCopyFunc(dbCopyName); + const runHollowCopy: string = runHollowCopyFunc(dbCopyName); - const runScript: string = `docker exec postgres-1 psql -U postgres -d ${dbName} -c "${trimSchemaEntry}"`; + // determine if the file is a sql or a tar file, in the case of a copy, we will not have a filepath so we just hard-code the extension to be sql let extension: string = ''; if (filePath.length > 0) { extension = filePath[0].slice(filePath[0].lastIndexOf('.')); } + else extension = '.sql'; // SEQUENCE OF EXECUTING COMMANDS // Steps are in reverse order because each step is a callback function that requires the following step to be defined. - // 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. + // Step 5: Changes the pg URI to look to the newly created database and queries all the tables in that database and sends it to frontend. async function sendLists() { listObj = await db.getLists(); event.sender.send('db-lists', listObj); + // tell the front end to switch tabs to the newly created database + event.sender.send('switch-to-new', null); + // notify frontend that async process has been completed + event.sender.send('async-complete'); }; - // Step 3 : Given the file path extension, run the appropriate command in postgres to build the db - const step3 = () => { + // Step 4: Given the file path extension, run the appropriate command in postgres to build the db + const step4 = () => { let runCmd: string = ''; if (extension === '.sql') runCmd = runSQL; else if (extension === '.tar') runCmd = runTAR; - else runCmd = runScript; execute(runCmd, sendLists); }; - // Step 2 : Import database file from file path into docker container - const step2 = () => execute(importFile, step3); + // Step 3: Import database file from file path into docker container + const step3 = () => execute(importFile, step4); + // skip step three which is only for importing files and instead change the current db to the newly created one + const step3Copy = () => { + db.changeDB(dbName); + return step4(); + } + + // Step 2: Change curent URI to match newly created DB + const step2 = () => { + // if we are copying + if (copy !== undefined) { + // first, we need to change the current DB instance to that of the one we need to copy, so we'll head to the changeDB function in the models file + db.changeDB(dbCopyName); + // now that our DB has been changed to the one we wish to copy, we need to either make an exact copy or a hollow copy using pg_dump OR pg_dump -s + // this generates a pg_dump file from the specified db and saves it to a location in the container. + // Full copy case + if (copy) { + execute(runFullCopy, step3Copy); + } + // Hollow copy case + else execute(runHollowCopy, step3Copy); + return; + } + // if we are not copying + else { + // change the current database back to the newly created one + // and now that we have changed to the new db, we can move on to importing the data file + db.changeDB(dbName); + return step3(); + } + } // Step 1 : Create empty db - if (extension === '.sql' || extension === '.tar') execute(createDB, step2); - // if data is inputted as text - else execute(createDB, step3); + execute(createDB, step2); }); interface QueryType { @@ -159,8 +225,34 @@ interface QueryType { queryStatistics: string; } +ipcMain.on('execute-query-untracked', (event, data: QueryType) => { + + // send notice to front end that query has been started + event.sender.send('async-started'); + + // destructure object from frontend + const { queryString } = data; + // run query on db + db.query(queryString) + .then(() => { + (async function getListAsync() { + listObj = await db.getLists(); + event.sender.send('db-lists', listObj); + event.sender.send('async-complete'); + })(); + }) + .catch((error: string) => { + console.log('ERROR in execute-query-untracked channel in main.ts', error); + event.sender.send('query-error', 'Error executing query.'); + }); +}); + // Listen for queries being sent from renderer -ipcMain.on('execute-query', (event, data: QueryType) => { +ipcMain.on('execute-query-tracked', (event, data: QueryType) => { + + // send notice to front end that query has been started + event.sender.send('async-started'); + // destructure object from frontend const { queryString, queryCurrentSchema, queryLabel } = data; @@ -178,22 +270,73 @@ ipcMain.on('execute-query', (event, data: QueryType) => { db.query(queryString) .then((queryData) => { frontendData.queryData = queryData.rows; - - // Run EXPLAIN (FORMAT JSON, ANALYZE) - db.query('EXPLAIN (FORMAT JSON, ANALYZE) ' + queryString).then((queryStats) => { - frontendData.queryStatistics = queryStats.rows; - + if (!queryString.match(/create/i)) { + // Run EXPLAIN (FORMAT JSON, ANALYZE) + db.query('EXPLAIN (FORMAT JSON, ANALYZE) ' + queryString) + .then((queryStats) => { + frontendData.queryStatistics = queryStats.rows; + + (async function getListAsync() { + listObj = await db.getLists(); + frontendData.lists = listObj; + event.sender.send('db-lists', listObj) + event.sender.send('return-execute-query', frontendData); + event.sender.send('async-complete'); + })(); + }) + } else { + // Handling for tracking a create table query, can't run explain/analyze on create statements (async function getListAsync() { listObj = await db.getLists(); frontendData.lists = listObj; - event.sender.send('db-lists', listObj) - event.sender.send('return-execute-query', frontendData); + event.sender.send('db-lists', listObj); + event.sender.send('async-complete'); })(); - }); + } }) .catch((error: string) => { - console.log('ERROR in execute-query channel in main.ts', error); + console.log('ERROR in execute-query-tracked channel in main.ts', error); }); + }); -module.exports; \ No newline at end of file +interface dummyDataRequest { + schemaName: string; + dummyData: {}; +} + +ipcMain.on('generate-dummy-data', (event: any, data: dummyDataRequest) => { + + // send notice to front end that DD generation has been started + event.sender.send('async-started'); + + let schemaLayout: any; + let dummyDataRequest: dummyDataRequest = data; + let tableMatricesArray: any; + let keyObject: any = "Unresolved"; + + db.createKeyObject() + .then((result) => { + // set keyObject equal to the result of this query + keyObject = result; + db.dropKeyColumns(keyObject) + .then(() => { + db.addNewKeyColumns(keyObject) + .then(() => { + db.getSchemaLayout() + .then((result) => { + schemaLayout = result; + // generate the dummy data and save it into matrices associated with table names + tableMatricesArray = generateDummyData(schemaLayout, dummyDataRequest, keyObject); + //iterate through tableMatricesArray to write individual .csv files + for (const tableObject of tableMatricesArray) { + // write all entries in tableMatrix to csv file + writeCSVFile(tableObject, schemaLayout, keyObject, dummyDataRequest, event); + } + }); + }); + }); + }) +}) + +export default execute; \ No newline at end of file diff --git a/backend/dummy_db/GenerateObj.ts b/backend/dummy_db/GenerateObj.ts deleted file mode 100644 index 91582dfd..00000000 --- a/backend/dummy_db/GenerateObj.ts +++ /dev/null @@ -1,81 +0,0 @@ -const faker = require('faker'); -const fakerLink = require('./fakerLink'); - -export const typeOptions : any = { - dropdown : ['Select one', 'unique', 'random'], - unique : { - dropdown : ['str', 'num'], - str : [ - { - display : true, - option : 'Minimum Length', - type : 'text', - location : 'minLen', - format : "false", - }, - { - display : true, - option : 'Maximum Length', - type : 'text', - location : 'maxLen', - format : "false", - }, - { - display : true, - option : 'Include lower case letters', - type : 'checkbox', - location : 'inclAlphaLow', - format : "false", - }, - { - display : true, - option : 'Include upper case letters', - type : 'checkbox', - location : 'inclAlphaUp', - format : "false", - }, - { - display : true, - option : 'Include numbers', - type : 'checkbox', - location : 'inclNum', - format : "false", - }, - { - display : true, - option : 'Include spaces', - type : 'checkbox', - location : 'inclSpaces', - format : "false", - }, - { - display : true, - option : 'Include special characters', - type : 'checkbox', - location : "specChar", - format : "false", - }, - { - display : true, - option : 'Include these values (separate by commas)', - type : 'text', - location : 'include', - format : "array", - }, - ], - num : [ - { - display : false, - option : 'Serial', - type : 'checkbox', - location : "serial", - format : "false", - value : true, - }, - ], - }, - random : { - dropdown : Object.keys(fakerLink.fakerLink), - message : 'For a sample of each random data type, please visit the Faker.js demo.' - }, - }; \ No newline at end of file diff --git a/backend/dummy_db/dataGenHandler.ts b/backend/dummy_db/dataGenHandler.ts deleted file mode 100644 index 0a76f87c..00000000 --- a/backend/dummy_db/dataGenHandler.ts +++ /dev/null @@ -1,131 +0,0 @@ -const faker = require('faker'); -const {fakerLink} = require('./fakerLink'); -const {types} = require('./dataTypeLibrary'); - -/* --- MAIN FUNCTION --- */ - -// GENERATE 'INSERT INTO' QUERY STRING -// Populate an array of INSERT queries to add the data for the table to the database. - // An array is used to break the insert into smaller pieces if needed. - // Postgres limits insert queries to 100,000 entry values: 100,000 / # of columns = Max number of rows per query. -// Arguments: form = DB generation form object submitted by user - from front end -export const createInsertQuery = (form : any) : string => { - const values = valuesList(form.columns, form.scale); - const cols = columnList(form.columns); - const queryArray : any = []; - values.forEach(e => queryArray.push(`INSERT INTO "${form.table}"(${cols}) VALUES ${e}; `)); - return queryArray; -} - - -/* --- CALLBACK FUNCTIONS --- */ - -// CREATE 'COLUMN' STRING FOR QUERY - // Called by createInsertQuery() -// deconstruct and convert the column names to a single string -// Arguments: column = form.columns -const columnList = (columns : Array) => { - let list : string = ''; - columns.forEach( (e : any , i : number) => { - list += e.name; - if (i < columns.length - 1) list += ', '; - } ); - return list; -} - -// CREATE ALL VALUES FOR ALL RECORDS AT SCALE - // Called by createInsertQuery() -// Arguments: column = form.columns, scale = form.scale -const valuesList = (columns : any, scale : number) => { - const columnTypes = createRecordFunc(columns, scale); - const valuesArray : any = []; - // determine maximum number of records Postgres will allow per insert query - with buffer - let maxRecords : number = 10; // columns.length; - let list : string = ''; - // create the number of records equal to the scale of the table - for (let i : number = 0; i < scale; i += 1) { - // start each record as an empty string - let record : string = ''; - // traverse each column and concat the results of calling the the data type function - columnTypes.forEach( (e : any, k : number) => { - // concat to the record the results of calling the function for the data type - // if the type is random, pass no arguments. If it is any other type, pass the index - let entry = (e.random) ? e.func().replace(`'`, ``) : e.func(i); - record += "" + ((typeof entry === 'string') ? `'${entry}'` : entry); - if (k < columns.length - 1) record += ', '; - }) - list += `(${record})`; - if (i && i % maxRecords === 0 || i === scale - 1) { - valuesArray.push(list); - list = ''; - } - else list += ', '; - } - return valuesArray; -}; - -// DEFINE TYPE FORMULAS FOR EACH COLUMN (prior to iterating) - // Called by valuesList() -// Helper function: connect each column to its appropriate function prior to creating records to reduce redundant function calls. -// Arguments: column = form.columns, scale = form.scale -const createRecordFunc = (columns : any, scale : number) => { - let output : Array = []; - columns.forEach(e => { - const {dataCategory, dataType} = e; - if (dataCategory === 'random') output.push({random : true, func : fakerLink[dataType]}); - else if (dataCategory === 'repeating' || dataCategory === 'unique') output.push({random : false, func : types[dataCategory][dataType](e.data, scale)}); - // ADD OTHER DATA TYPES HERE - else { - console.log(`ERROR: Column ${e.name} has an invalid data type. Table will still populate but this column will be empty.`) - output.push (() => {}); - } - } ); - return output; -}; - -/* UNCOMMENT BELOW FOR TESTING OBJECT AND FUNCTION */ -// const fromApp = { -// schema : 'schema1', // Not currrently relevant: when multiple schemas per db are added, add this after INTO in createInsertQuery = |"${form.schema}".| -// table : 'table1', -// scale : 5, -// columns : [ -// { -// name : '_id', -// dataCategory : 'unique', // random, repeating, unique, combo, foreign -// dataType : 'num', -// data : { -// serial: true, -// } -// }, -// { -// name : 'username', -// dataCategory : 'unique', // random, repeating, unique, combo, foreign -// dataType : 'str', -// data : { -// minLen : 10, -// maxLen : 15, -// inclAlphaLow : true, -// inclAlphaUp : true, -// inclNum : true, -// inclSpaces : true, -// inclSpecChar : true, -// include : ["include", "these", "abReplace"], -// }, -// }, -// { -// name : 'first_name', -// dataCategory : 'random', // random, repeating, unique, combo, foreign -// dataType : 'Name - firstName', -// data : { -// } -// }, -// { -// name : 'company_name', -// dataCategory : 'random', -// dataType : 'Company - companyName', -// data : { -// } -// } -// ] -// }; -// console.log(createInsertQuery(fromApp)); diff --git a/backend/dummy_db/dataTypeLibrary.ts b/backend/dummy_db/dataTypeLibrary.ts deleted file mode 100644 index 5628209a..00000000 --- a/backend/dummy_db/dataTypeLibrary.ts +++ /dev/null @@ -1,86 +0,0 @@ -interface Types { - unique : any; - repeating : object; -} - -export const types : Types = { - unique : {}, - repeating : {}, -}; - -// Closure function that contains all functionality for creating unique strings -types.unique.str = (data : any, scale : number) => { - let chars : string = ''; - const unique : Array = []; - const lockedIndexes : Array = []; - // if character types are 'true', append them to the character list - if (data.inclAlphaLow) chars += 'abcdefghijklmnopqrstuvwxyz'; - if (data.inclAlphaUp) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - if (data.inclNum) chars += '0123456789'; - if (data.inclSpaces) chars += ' '; - if (data.inclSpecChar) chars += ',.?;:!@#$%^&*'; - // if none were true or is only inclSpaces was true, a series of unique values will not be possible. - // if this is the case, set chars to include all letters - lower and upper - if (chars.length <= 1) chars += 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - // ensure that the minimum length can accommodate unique values to the length of the scale - let min : number = 1; - while (chars.length ** min < scale) min += 1; - // create minimum unique keys in sequence for quick retrieval when creating record - // stop once scale is reached - (function buildUnique (str : string) { - if (str.length === min) { - unique.push(str); - return; - } - for (let i : number = 0; i < chars.length; i += 1) { - if (unique.length === scale) return; - buildUnique(str + chars[i]); - } - })(''); - // handle INCLUDE values : values the user requires to exist - // find the first chars up to the index of min (prefix) then search the unique array for that prefix. - // if it exist, replace it with the full string. - // if not, find a random index and insert the full string there. - // Keep track of the indexes already use to avoid overwriting something we need to save (lockedIndex on output) - if (data.include > scale) console.log(`ERROR: Entries in 'Include' exceed the scale of the table, some values will not be represented.` ) - data.include.sort(); - for (let i = 0; i < data.include.length && i < scale; i += 1) { - let prefix : string = ''; - for (let k : number = 0; k < min && k < data.include[i].length; k += 1) prefix += data.include[i][k]; - let index : number = unique.indexOf(prefix); - while (lockedIndexes.includes(index) || index === -1) { - index = Math.floor(Math.random() * Math.floor(scale)); - } - lockedIndexes.push(index); - unique[index] = data.include[i]; - } - lockedIndexes.sort(); - - // CLOSURE : function to be called on each record - - return function (i) { - // initalize the output string with the unique prefix associated with that record (i) - let output : string = unique[i]; - // if the value has already be set from INCLUDES, do not append random digits. - if (lockedIndexes.includes(i)) return output; - // create a random string length between the user specified min/max bounds. - // account for the space already taken by the prefix - const strLen : number = Math.round(Math.random() * (data.maxLen - data.minLen)) + data.minLen; - for (let k = unique[i].length; k < strLen; k += 1) { - output += chars[Math.floor(Math.random() * Math.floor(chars.length))] - } - return output; - }; -}; - -types.unique.num = (data : any, scale : number) => { - return (index) => {if (data.serial) return index}; -}; - - - - -// REPEATING DATA TYPE - STILL NEEDED -// types.repeating.loop = (data : object, scale : number) => {}; -// types.repeating.weighted = (data : object, scale : number) => {}; -// types.repeating.counted = (data : object, scale : number) => {}; \ No newline at end of file diff --git a/backend/dummy_db/fakerLink.ts b/backend/dummy_db/fakerLink.ts deleted file mode 100644 index e45b9dad..00000000 --- a/backend/dummy_db/fakerLink.ts +++ /dev/null @@ -1,187 +0,0 @@ -const faker = require('faker'); - -export const fakerLink = { - 'Address - zipCode' : faker.address.zipCode, - 'Address - zipCodeByState' : faker.address.zipCodeByState, - 'Address - city' : faker.address.city, - 'Address - cityPrefix' : faker.address.cityPrefix, - 'Address - citySuffix' : faker.address.citySuffix, - 'Address - streetName' : faker.address.streetName, - 'Address - streetAddress' : faker.address.streetAddress, - 'Address - streetSuffix' : faker.address.streetSuffix, - 'Address - streetPrefix' : faker.address.streetPrefix, - 'Address - secondaryAddress' : faker.address.secondaryAddress, - 'Address - county' : faker.address.county, - 'Address - country' : faker.address.country, - 'Address - countryCode' : faker.address.countryCode, - 'Address - state' : faker.address.state, - 'Address - stateAbbr' : faker.address.stateAbbr, - 'Address - latitude' : faker.address.latitude, - 'Address - longitude' : faker.address.longitude, - 'Address - direction' : faker.address.direction, - 'Address - cardinalDirection' : faker.address.cardinalDirection, - 'Address - ordinalDirection' : faker.address.ordinalDirection, - 'Address - nearbyGPSCoordinate' : faker.address.nearbyGPSCoordinate, - 'Address - timeZone' : faker.address.timeZone, - 'Commerce - color' : faker.commerce.color, - 'Commerce - department' : faker.commerce.department, - 'Commerce - productName' : faker.commerce.productName, - 'Commerce - price' : faker.commerce.price, - 'Commerce - productAdjective' : faker.commerce.productAdjective, - 'Commerce - productMaterial' : faker.commerce.productMaterial, - 'Commerce - product' : faker.commerce.product, - 'Commerce - productDescription' : faker.commerce.productDescription, - 'Company - suffixes' : faker.company.suffixes, - 'Company - companyName' : faker.company.companyName, - 'Company - companySuffix' : faker.company.companySuffix, - 'Company - catchPhrase' : faker.company.catchPhrase, - 'Company - bs' : faker.company.bs, - 'Company - catchPhraseAdjective' : faker.company.catchPhraseAdjective, - 'Company - catchPhraseDescriptor' : faker.company.catchPhraseDescriptor, - 'Company - catchPhraseNoun' : faker.company.catchPhraseNoun, - 'Company - bsAdjective' : faker.company.bsAdjective, - 'Company - bsBuzz' : faker.company.bsBuzz, - 'Company - bsNoun' : faker.company.bsNoun, - 'Database - column' : faker.database.column, - 'Database - type' : faker.database.type, - 'Database - collation' : faker.database.collation, - 'Database - engine' : faker.database.engine, - 'Date - past' : faker.date.past, - 'Date - future' : faker.date.future, - 'Date - between' : faker.date.between, - 'Date - recent' : faker.date.recent, - 'Date - soon' : faker.date.soon, - 'Date - month' : faker.date.month, - 'Date - weekday' : faker.date.weekday, - 'Finance - account' : faker.finance.account, - 'Finance - accountName' : faker.finance.accountName, - 'Finance - routingNumber' : faker.finance.routingNumber, - 'Finance - mask' : faker.finance.mask, - 'Finance - amount' : faker.finance.amount, - 'Finance - transactionType' : faker.finance.transactionType, - 'Finance - currencyCode' : faker.finance.currencyCode, - 'Finance - currencyName' : faker.finance.currencyName, - 'Finance - currencySymbol' : faker.finance.currencySymbol, - 'Finance - bitcoinAddress' : faker.finance.bitcoinAddress, - 'Finance - litecoinAddress' : faker.finance.litecoinAddress, - 'Finance - creditCardNumber' : faker.finance.creditCardNumber, - 'Finance - creditCardCVV' : faker.finance.creditCardCVV, - 'Finance - ethereumAddress' : faker.finance.ethereumAddress, - 'Finance - iban' : faker.finance.iban, - 'Finance - bic' : faker.finance.bic, - 'Finance - transactionDescription' : faker.finance.transactionDescription, - 'Git - branch' : faker.git.branch, - 'Git - commitEntry' : faker.git.commitEntry, - 'Git - commitMessage' : faker.git.commitMessage, - 'Git - commitSha' : faker.git.commitSha, - 'Git - shortSha' : faker.git.shortSha, - 'Hacker - abbreviation' : faker.hacker.abbreviation, - 'Hacker - adjective' : faker.hacker.adjective, - 'Hacker - noun' : faker.hacker.noun, - 'Hacker - verb' : faker.hacker.verb, - 'Hacker - ingverb' : faker.hacker.ingverb, - 'Hacker - phrase' : faker.hacker.phrase, - 'Helpers - randomize' : faker.helpers.randomize, - 'Helpers - slugify' : faker.helpers.slugify, - 'Helpers - replaceSymbolWithNumber' : faker.helpers.replaceSymbolWithNumber, - 'Helpers - replaceSymbols' : faker.helpers.replaceSymbols, - 'Helpers - replaceCreditCardSymbols' : faker.helpers.replaceCreditCardSymbols, - 'Helpers - repeatString' : faker.helpers.repeatString, - 'Helpers - regexpStyleStringParse' : faker.helpers.regexpStyleStringParse, - 'Helpers - shuffle' : faker.helpers.shuffle, - 'Helpers - mustache' : faker.helpers.mustache, - 'Helpers - createCard' : faker.helpers.createCard, - 'Helpers - contextualCard' : faker.helpers.contextualCard, - 'Helpers - userCard' : faker.helpers.userCard, - 'Helpers - createTransaction' : faker.helpers.createTransaction, - 'Image - image' : faker.image.image, - 'Image - avatar' : faker.image.avatar, - 'Image - imageUrl' : faker.image.imageUrl, - 'Image - abstract' : faker.image.abstract, - 'Image - animals' : faker.image.animals, - 'Image - business' : faker.image.business, - 'Image - cats' : faker.image.cats, - 'Image - city' : faker.image.city, - 'Image - food' : faker.image.food, - 'Image - nightlife' : faker.image.nightlife, - 'Image - fashion' : faker.image.fashion, - 'Image - people' : faker.image.people, - 'Image - nature' : faker.image.nature, - 'Image - sports' : faker.image.sports, - 'Image - technics' : faker.image.technics, - 'Image - transport' : faker.image.transport, - 'Image - dataUri' : faker.image.dataUri, - 'Image - lorempixel' : faker.image.lorempixel, - 'Image - unsplash' : faker.image.unsplash, - 'Image - lorempicsum' : faker.image.lorempicsum, - 'Internet - avatar' : faker.internet.avatar, - 'Internet - email' : faker.internet.email, - 'Internet - exampleEmail' : faker.internet.exampleEmail, - 'Internet - userName' : faker.internet.userName, - 'Internet - protocol' : faker.internet.protocol, - 'Internet - url' : faker.internet.url, - 'Internet - domainName' : faker.internet.domainName, - 'Internet - domainSuffix' : faker.internet.domainSuffix, - 'Internet - domainWord' : faker.internet.domainWord, - 'Internet - ip' : faker.internet.ip, - 'Internet - ipv6' : faker.internet.ipv6, - 'Internet - userAgent' : faker.internet.userAgent, - 'Internet - color' : faker.internet.color, - 'Internet - mac' : faker.internet.mac, - 'Internet - password' : faker.internet.password, - 'Lorem - word' : faker.lorem.word, - 'Lorem - words' : faker.lorem.words, - 'Lorem - sentence' : faker.lorem.sentence, - 'Lorem - slug' : faker.lorem.slug, - 'Lorem - sentences' : faker.lorem.sentences, - 'Lorem - paragraph' : faker.lorem.paragraph, - 'Lorem - paragraphs' : faker.lorem.paragraphs, - 'Lorem - text' : faker.lorem.text, - 'Lorem - lines' : faker.lorem.lines, - 'Name - firstName' : faker.name.firstName, - 'Name - lastName' : faker.name.lastName, - 'Name - findName' : faker.name.findName, - 'Name - jobTitle' : faker.name.jobTitle, - 'Name - gender' : faker.name.gender, - 'Name - prefix' : faker.name.prefix, - 'Name - suffix' : faker.name.suffix, - 'Name - title' : faker.name.title, - 'Name - jobDescriptor' : faker.name.jobDescriptor, - 'Name - jobArea' : faker.name.jobArea, - 'Name - jobType' : faker.name.jobType, - 'Phone - phoneNumber' : faker.phone.phoneNumber, - 'Phone - phoneNumberFormat' : faker.phone.phoneNumberFormat, - 'Phone - phoneFormats' : faker.phone.phoneFormats, - 'Random - number' : faker.random.number, - 'Random - float' : faker.random.float, - 'Random - arrayElement' : faker.random.arrayElement, - 'Random - arrayElements' : faker.random.arrayElements, - 'Random - objectElement' : faker.random.objectElement, - 'Random - uuid' : faker.random.uuid, - 'Random - boolean' : faker.random.boolean, - 'Random - word' : faker.random.word, - 'Random - words' : faker.random.words, - 'Random - image' : faker.random.image, - 'Random - locale' : faker.random.locale, - 'Random - alpha' : faker.random.alpha, - 'Random - alphaNumeric' : faker.random.alphaNumeric, - 'Random - hexaDecimal' : faker.random.hexaDecimal, - 'System - fileName' : faker.system.fileName, - 'System - commonFileName' : faker.system.commonFileName, - 'System - mimeType' : faker.system.mimeType, - 'System - commonFileType' : faker.system.commonFileType, - 'System - commonFileExt' : faker.system.commonFileExt, - 'System - fileType' : faker.system.fileType, - 'System - fileExt' : faker.system.fileExt, - 'System - directoryPath' : faker.system.directoryPath, - 'System - filePath' : faker.system.filePath, - 'System - semver' : faker.system.semver, - 'Time - recent' : faker.time.recent, - 'Vehicle - vehicle' : faker.vehicle.vehicle, - 'Vehicle - manufacturer' : faker.vehicle.manufacturer, - 'Vehicle - model' : faker.vehicle.model, - 'Vehicle - type' : faker.vehicle.type, - 'Vehicle - fuel' : faker.vehicle.fuel, - 'Vehicle - vin' : faker.vehicle.vin, - 'Vehicle - color' : faker.vehicle.color, -}; \ No newline at end of file diff --git a/backend/dummy_db/temp_connectToDB.ts b/backend/dummy_db/temp_connectToDB.ts deleted file mode 100644 index 905c769d..00000000 --- a/backend/dummy_db/temp_connectToDB.ts +++ /dev/null @@ -1,105 +0,0 @@ -// // Temporary Hardcoded database scaling -// // To test this code: -// // 1. Copy and paste this code into the bottom of main.ts -// // 2. Create a button on the frontend that activates the route 'generate-data' -// // 3. Test the button out out on the defaultDB tab, should only work in that tab because 'defaultDB' is hardcoded below - -// /*=== SAMPLE OBJECT TO BE SENT FROM USER INTERFACE TO DATA GENERATOR ===*/ -// const fromApp = { -// schema: 'public', //used to be schema1 -// table: 'table1', -// scale: 40, -// columns: [ -// { -// name: '_id', -// dataCategory: 'unique', // random, repeating, unique, combo, foreign -// dataType: 'num', -// data: { -// serial: true, -// } -// }, -// { -// name: 'username', -// dataCategory: 'unique', // random, repeating, unique, combo, foreign -// dataType: 'str', -// data: { -// length: [10, 15], -// inclAlphaLow: true, -// inclAlphaUp: true, -// inclNum: true, -// inclSpaces: true, -// inclSpecChar: true, -// include: ["include", "these", "aReplace"], -// }, -// }, -// { -// name: 'first_name', -// dataCategory: 'random', // random, repeating, unique, combo, foreign -// dataType: 'Name - firstName', -// data: { -// } -// }, -// { -// name: 'company_name', -// dataCategory: 'random', -// dataType: 'Company - companyName', -// data: { -// } -// } -// ] -// }; - - -// ipcMain.on('generate-data', (event, paramObj: any) => { -// // Generating Dummy Data from parameters sent from the frontend -// (function dummyFunc(paramsObj) { // paramsObj === fromApp -// // Need addDB in this context -// const addDB = (str: string, nextStep: any) => { -// exec(str, (error, stdout, stderr) => { -// if (error) { -// console.log(`error: ${error.message}`); -// return; -// } -// if (stderr) { -// console.log(`stderr: ${stderr}`); -// return; -// } -// // console.log(`stdout: ${stdout}`); -// console.log(`${stdout}`); -// if (nextStep) nextStep(); -// }); -// }; -// const db_name: string = 'defaultDB'; -// // This is based off of the fromApp hard coded object, -// // In theory this would be given to SeeQR from the user -// const schemaStr: string = `CREATE TABLE "table1"( -// "_id" integer NOT NULL, -// "username" VARCHAR(255) NOT NULL, -// "first_name" VARCHAR(255) NOT NULL, -// "company_name" VARCHAR(255) NOT NULL, -// CONSTRAINT "tabl1_pk" PRIMARY KEY ("_id") -// ) WITH ( -// OIDS=FALSE -// );` -// // This is where createInsertQuery function is invoked -// const insertArray: Array = createInsertQuery(paramsObj); -// console.log(insertArray); -// // Important part !!!! -// // takes in an array of insert query strings: insertArray -// // this insertArray is the output of the createInsertQuery function from dataGenHandler.ts -// // db_name is whatever tab they're currently on -// // scemaStr is the hard coded table for the fromApp hard coded object -// db.query(schemaStr) // this makes hard coded table in database -// .then((returnedData) => { -// // ====== BELOW IS MAIN FUNCTIONALITY FOR SUBMITTING DUMMY DATA TO THE DATABASE ======= AKA looping insert queries into the node child process -// // USE THIS ALONG WITH THE addDB(node childprocess) FUNCTION FOR FINAL PRODUCT -// // THE CODE FROM ABOVE IS FOR TESTING THIS WITHOUT THE INTERFACE -// for (let i = 0; i < insertArray.length; ++i) { -// console.log(i) -// let currentInsert = insertArray[i]; -// const dummyScript: string = `docker exec postgres-1 psql -U postgres -d ${db_name} -c "${currentInsert}"`; -// addDB(dummyScript, () => console.log(`Dummied Database: ${db_name}`)) //using the Node childprocess to access postgres for each INSERT query in the insertArray -// } -// }) -// })(fromApp); -// }); \ No newline at end of file diff --git a/backend/main.ts b/backend/main.ts index 58152cc1..4ecd600d 100644 --- a/backend/main.ts +++ b/backend/main.ts @@ -1,13 +1,14 @@ // Import parts of electron to use import { app, BrowserWindow, ipcMain, Menu } from 'electron'; +import { appendFile } from 'fs/promises'; import { join } from 'path'; import { format } from 'url'; -import './channels' // all channels live here +//import './channels' // all channels live here +import execute from './channels'; const { exec } = require('child_process'); const appMenu = require('./mainMenu'); // use appMenu to add options in top menu bar of app const path = require('path'); -const createInsertQuery = require('./dummy_db/dataGenHandler'); /************************************************************ *********** PACKAGE ELECTRON APP FOR DEPLOYMENT *********** @@ -24,6 +25,9 @@ const createInsertQuery = require('./dummy_db/dataGenHandler'); // be closed automatically when the JavaScript object is garbage collected. let mainWindow: any; +//global variable to determine whether or not containers are still running +let pruned: boolean = false; + let mainMenu = Menu.buildFromTemplate(require('./mainMenu')); // Keep a reference for dev mode let dev = false; @@ -73,6 +77,9 @@ function createWindow() { // Don't show until we are ready and loaded mainWindow.once('ready-to-show', (event) => { mainWindow.show(); + // uncomment code below before running production build and packaging + // const yamlPath = join(__dirname, '../../docker-compose.yml') + // const runDocker: string = `docker-compose -f '${yamlPath}' up -d`; const runDocker: string = `docker-compose up -d`; exec(runDocker, (error, stdout, stderr) => { if (error) { @@ -86,28 +93,33 @@ function createWindow() { console.log(`${stdout}`); }) }); +} - // Emitted when the window is closed. - mainWindow.on('closed', function () { +app.on('before-quit', (event: any) => { + // check if containers have already been pruned--else, continue with default behavior to terminate application + if (!pruned) { + event.preventDefault(); // Stop and remove postgres-1 and busybox-1 Docker containers upon window exit. + const stopContainers: string = 'docker stop postgres-1 busybox-1'; const pruneContainers: string = 'docker rm -f postgres-1 busybox-1'; - const executeQuery = (str) => { - exec(str, (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - return; - } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - console.log(`${stdout}`); - }) + // this command removes the volume which stores the session data for the postgres instance + // comment this out for dev + const pruneVolumes: string = 'docker volume rm -f seeqr_database-data'; + + // use this string for production build + // const pruneVolumes: string = 'docker volume rm -f app_database-data' + + const step4 = () => { + pruned = true; + app.quit() }; - executeQuery(pruneContainers); - mainWindow = null; - }); -} + const step3 = () => execute(pruneVolumes, step4); + const step2 = () => execute(pruneContainers, step3); + + execute(stopContainers, step2); + } +}) + // Invoke createWindow to create browser windows after Electron has been initialized. // Some APIs can only be used after this event occurs. @@ -130,3 +142,4 @@ app.on('activate', () => { } }); +export default mainWindow; \ No newline at end of file diff --git a/backend/models.ts b/backend/models.ts index f5c9a33f..772d0b9b 100644 --- a/backend/models.ts +++ b/backend/models.ts @@ -1,49 +1,284 @@ const { Pool } = require('pg'); +const { getPrimaryKeys, getForeignKeys } = require('./DummyD/foreign_key_info') // Initialize to a default db. // URI Format: postgres://username:password@hostname:port/databasename let PG_URI: string = 'postgres://postgres:postgres@localhost:5432/defaultDB'; let pool: any = new Pool({ connectionString: PG_URI }); +//helper function that creates the column objects, which are saved to the schemaLayout object +//this function returns a promise to be resolved with Promise.all syntax +const getColumnObjects = (tableName: string) => { + const queryString = "SELECT column_name, data_type, character_maximum_length FROM information_schema.columns WHERE table_name = $1;"; + const value = [tableName]; + return new Promise ((resolve) => { + pool + .query(queryString, value) + .then((result) => { + const columnInfoArray: any = []; + for (let i = 0; i < result.rows.length; i++) { + const columnObj: any = { + columnName: result.rows[i].column_name, + dataInfo: { + data_type: result.rows[i].data_type, + character_maxiumum_length: result.rows[i].character_maxiumum_length + } + } + columnInfoArray.push(columnObj) + } + resolve(columnInfoArray); + }) + }) +} + +// gets all the names of the current postgres instances +const getDBNames = () => { + return new Promise((resolve) =>{ + pool + .query('SELECT datname FROM pg_database;') + .then((databases) => { + let dbList: any = []; + for (let i = 0; i < databases.rows.length; ++i) { + let curName = databases.rows[i].datname; + if (curName !== 'postgres' && curName !== 'template0' && curName !== 'template1') + dbList.push(databases.rows[i].datname); + } + resolve(dbList); + }) + }) +} + +// gets all tablenames from currentschema +const getDBLists = () => { + return new Promise((resolve) => { + pool + .query("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;" + ) + .then((tables) => { + let tableList: any = []; + for (let i = 0; i < tables.rows.length; ++i) { + tableList.push(tables.rows[i].table_name); + } + resolve(tableList); + }) + }) +} + module.exports = { + query: (text, params, callback) => { console.log('Executed query: ', text); return pool.query(text, params, callback); }, + changeDB: (dbName: string) => { PG_URI = 'postgres://postgres:postgres@localhost:5432/' + dbName; pool = new Pool({ connectionString: PG_URI }); console.log('Current URI: ', PG_URI); + return dbName; }, + getLists: () => { return new Promise((resolve) => { - const listObj = { + const listObj: any = { tableList: [], // current database's tables databaseList: [], }; - // This query returns the names of all the tables in the database, so that the frontend can make a visual for the user + Promise.all([getDBNames(), getDBLists()]) + .then((data) => { + console.log('models: ', data); + listObj.databaseList = data[0]; + listObj.tableList = data[1]; + resolve(listObj); + }) + }) + }, + + + createKeyObject: () => { + return new Promise((resolve) => { + // initialize the keyObject we eventually want to return out + const keyObject: any = {}; pool + .query(getPrimaryKeys, null) + .then((result) => { + let table; + let pkColumn + // iterate over the primary key table, adding info to our keyObject + for (let i = 0; i < result.rows.length; i++) { + table = result.rows[i].table_name; + pkColumn = result.rows[i].pk_column; + // if the table is not yet initialized within the keyObject, then initialize it + if (!keyObject[table]) keyObject[table] = {primaryKeyColumns: {}, foreignKeyColumns: {}}; + // then just set the value at the pk column name to true for later checking + keyObject[table].primaryKeyColumns[pkColumn] = true; + } + }) + .then(() => { + pool + .query(getForeignKeys, null) + .then((result) => { + let table; + let primaryTable; + let fkColumn; + // iterate over the foreign key table, adding info to our keyObject + for (let i = 0; i < result.rows.length; i++) { + table = result.rows[i].foreign_table; + primaryTable = result.rows[i].primary_table + fkColumn = result.rows[i].fk_column; + // if the table is not yet initialized within the keyObject, then initialize it + if (!keyObject[table]) keyObject[table] = {primaryKeyColumns: {}, foreignKeyColumns: {}}; + // then set the value at the fk column name to the number of rows asked for in the primary table to which it points + keyObject[table].foreignKeyColumns[fkColumn] = primaryTable; + } + resolve(keyObject); + }) + }) + }) + }, + + dropKeyColumns: async (keyObject: any) => { + // define helper function to generate and run query + const generateAndRunDropQuery = (table: string) => { + let queryString = `ALTER TABLE ${table}`; + let count: number = 2; + + for (const pkc in keyObject[table].primaryKeyColumns){ + if (count > 2) queryString += ','; + queryString += ` DROP COLUMN ${pkc} CASCADE`; + count += 1; + } + for (const fkc in keyObject[table].foreignKeyColumns){ + if (count > 2) queryString += ','; + queryString += ` DROP COLUMN ${fkc}` + count += 1; + } + queryString += ';' + + return Promise.resolve(pool.query(queryString)); + } + + // iterate over tables, running drop queries, and pushing a new promise to promise array + for (const table in keyObject) { + await generateAndRunDropQuery(table); + } + + return; + }, + + addNewKeyColumns: async (keyObject: any) => { + // define helper function to generate and run query + const generateAndRunAddQuery = (table: string) => { + let queryString = `ALTER TABLE ${table}`; + let count: number = 2; + + for (const pkc in keyObject[table].primaryKeyColumns){ + if (count > 2) queryString += ','; + queryString += ` ADD COLUMN ${pkc} INT`; + count += 1; + } + for (const fkc in keyObject[table].foreignKeyColumns){ + if (count > 2) queryString += ','; + queryString += ` ADD COLUMN ${fkc} INT` + count += 1; + } + queryString += ';' + + return Promise.resolve(pool.query(queryString)); + } + + // iterate over tables, running drop queries, and pushing a new promise to promise array + for (const table in keyObject){ + await generateAndRunAddQuery(table); + } + + return; + }, + + getSchemaLayout: () => { + // initialize a new promise; we resolve this promise at the end of the last async function within the promise + return new Promise((resolve) => { + const schemaLayout: any = { + tableNames: [], + tables: { + // tableName: [columnObj array] + } + }; + pool + // This query returns the names of all the tables in the database .query( "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;" ) + // then we save the table names into the schemaLayout object in the tableNames property .then((tables) => { - let tableList: any = []; for (let i = 0; i < tables.rows.length; ++i) { - tableList.push(tables.rows[i].table_name); + schemaLayout.tableNames.push(tables.rows[i].table_name); } - listObj.tableList = tableList; - - pool.query('SELECT datname FROM pg_database;').then((databases) => { - let dbList: any = []; - for (let i = 0; i < databases.rows.length; ++i) { - let curName = databases.rows[i].datname; - if (curName !== 'postgres' && curName !== 'template0' && curName !== 'template1') - dbList.push(databases.rows[i].datname); - } - listObj.databaseList = dbList; - resolve(listObj); - }); - }); + const promiseArray: any = []; + for (let tableName of schemaLayout.tableNames) { + promiseArray.push(getColumnObjects(tableName)) + } + //we resolve all of the promises for the data info, and are returned an array of column data objects + Promise.all(promiseArray) + .then((columnInfo) => { + //here, we create a key for each table name and assign the array of column objects to the corresponding table name + for (let i = 0; i < columnInfo.length; i++) { + schemaLayout.tables[schemaLayout.tableNames[i]] = columnInfo[i]; + } + resolve(schemaLayout); + }) + }) + .catch(() => { + console.log('error in models.ts') + }) }); }, -}; \ No newline at end of file + + addPrimaryKeyConstraints: async (keyObject, dummyDataRequest) => { + // iterate over table's keyObject property, add primary key constraints + for (const tableName of Object.keys(dummyDataRequest.dummyData)) { + if (keyObject[tableName]) { + if (Object.keys(keyObject[tableName].primaryKeyColumns).length) { + let queryString: string = `ALTER TABLE ${tableName} `; + let count: number = 0; + + for (const pk in keyObject[tableName].primaryKeyColumns) { + if (count > 0) queryString += `, `; + queryString += `ADD CONSTRAINT "${tableName}_pk${count}" PRIMARY KEY ("${pk}")`; + count += 1; + } + + queryString += `;`; + // wait for the previous query to return before moving on to the next table + await pool.query(queryString); + } + } + } + return; + }, + + addForeignKeyConstraints: async (keyObject, dummyDataRequest) => { + // iterate over table's keyObject property, add foreign key constraints + for (const tableName of Object.keys(dummyDataRequest.dummyData)) { + if (keyObject[tableName]) { + if (Object.keys(keyObject[tableName].foreignKeyColumns).length) { + let queryString: string = `ALTER TABLE ${tableName} `; + let count: number = 0; + + for (const fk in keyObject[tableName].foreignKeyColumns) { + let primaryTable: string = keyObject[tableName].foreignKeyColumns[fk]; + let primaryKey: any = Object.keys(keyObject[primaryTable].primaryKeyColumns)[0]; + if (count > 0) queryString += `, `; + queryString += `ADD CONSTRAINT "${tableName}_fk${count}" FOREIGN KEY ("${fk}") REFERENCES ${primaryTable}("${primaryKey}")`; + count += 1; + } + + queryString += `;`; + // wait for the previous query to return before moving on to the next table + await pool.query(queryString); + } + } + } + return; + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index e589ad07..c59eccfe 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,7 @@ services: POSTGRES_USER: postgres POSTGRES_DB: defaultDB volumes: - - database-data + - database-data:/var/lib/postgresql/data depends_on: - bb container_name: postgres-1 diff --git a/frontend/assets/images/caseyescovedo.png b/frontend/assets/images/caseyescovedo.png new file mode 100644 index 00000000..58d1895a Binary files /dev/null and b/frontend/assets/images/caseyescovedo.png differ diff --git a/frontend/assets/images/caseywalker.png b/frontend/assets/images/caseywalker.png new file mode 100644 index 00000000..116efe5b Binary files /dev/null and b/frontend/assets/images/caseywalker.png differ diff --git a/frontend/assets/images/dummy_data_demo.gif b/frontend/assets/images/dummy_data_demo.gif new file mode 100644 index 00000000..d1e5af60 Binary files /dev/null and b/frontend/assets/images/dummy_data_demo.gif differ diff --git a/frontend/assets/images/graph_demo.gif b/frontend/assets/images/graph_demo.gif new file mode 100644 index 00000000..d8e94a71 Binary files /dev/null and b/frontend/assets/images/graph_demo.gif differ diff --git a/frontend/assets/images/input_schema_demo.gif b/frontend/assets/images/input_schema_demo.gif new file mode 100644 index 00000000..ad9ad466 Binary files /dev/null and b/frontend/assets/images/input_schema_demo.gif differ diff --git a/frontend/assets/images/interface.png b/frontend/assets/images/interface.png new file mode 100644 index 00000000..bb2be6f1 Binary files /dev/null and b/frontend/assets/images/interface.png differ diff --git a/frontend/assets/images/justinduryagri.png b/frontend/assets/images/justinduryagri.png new file mode 100644 index 00000000..55f691dd Binary files /dev/null and b/frontend/assets/images/justinduryagri.png differ diff --git a/frontend/assets/images/query1.png b/frontend/assets/images/query1.png deleted file mode 100644 index dff0f5df..00000000 Binary files a/frontend/assets/images/query1.png and /dev/null differ diff --git a/frontend/assets/images/query2.png b/frontend/assets/images/query2.png deleted file mode 100644 index 42d329ff..00000000 Binary files a/frontend/assets/images/query2.png and /dev/null differ diff --git a/frontend/assets/images/query_demo.gif b/frontend/assets/images/query_demo.gif new file mode 100644 index 00000000..2c5368fb Binary files /dev/null and b/frontend/assets/images/query_demo.gif differ diff --git a/frontend/assets/images/queryinput.png b/frontend/assets/images/queryinput.png deleted file mode 100644 index 80a2db42..00000000 Binary files a/frontend/assets/images/queryinput.png and /dev/null differ diff --git a/frontend/assets/images/queryruntime1.png b/frontend/assets/images/queryruntime1.png deleted file mode 100644 index 4ccf66b3..00000000 Binary files a/frontend/assets/images/queryruntime1.png and /dev/null differ diff --git a/frontend/assets/images/queryruntime2.png b/frontend/assets/images/queryruntime2.png deleted file mode 100644 index 2f1f1635..00000000 Binary files a/frontend/assets/images/queryruntime2.png and /dev/null differ diff --git a/frontend/assets/images/results.png b/frontend/assets/images/results.png deleted file mode 100644 index eb2d2e74..00000000 Binary files a/frontend/assets/images/results.png and /dev/null differ diff --git a/frontend/assets/images/samfrakes.png b/frontend/assets/images/samfrakes.png new file mode 100644 index 00000000..37844016 Binary files /dev/null and b/frontend/assets/images/samfrakes.png differ diff --git a/frontend/assets/images/schemamodal.png b/frontend/assets/images/schemamodal.png deleted file mode 100644 index ad49c8e1..00000000 Binary files a/frontend/assets/images/schemamodal.png and /dev/null differ diff --git a/frontend/assets/images/splash_page.png b/frontend/assets/images/splash_page.png new file mode 100644 index 00000000..016b0447 Binary files /dev/null and b/frontend/assets/images/splash_page.png differ diff --git a/frontend/assets/images/splash_screencap.png b/frontend/assets/images/splash_screencap.png deleted file mode 100644 index ff5c3412..00000000 Binary files a/frontend/assets/images/splash_screencap.png and /dev/null differ diff --git a/frontend/assets/images/wholeinterface.png b/frontend/assets/images/wholeinterface.png deleted file mode 100644 index 46544a4d..00000000 Binary files a/frontend/assets/images/wholeinterface.png and /dev/null differ diff --git a/frontend/assets/stylesheets/css/components.css b/frontend/assets/stylesheets/css/components.css index d630b813..ab8dfea8 100644 --- a/frontend/assets/stylesheets/css/components.css +++ b/frontend/assets/stylesheets/css/components.css @@ -26,6 +26,10 @@ background-color: #c6d2d5; } +#query-panel { + max-width: 90%; +} + #query-panel button { border: 0.5px #444c50 solid; background-color: #596368; @@ -89,7 +93,7 @@ input { background-color: #444c50; border: none; padding: 7px; - min-width: 240px; + min-width: 20em; outline: none; } @@ -97,17 +101,119 @@ input *:focus { outline: none; } +.dummy-data-select { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + padding: 1rem; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +#dummy-rows-input { + min-width: 10em; + margin-left: 1.5em; + margin-right: 1.5em; +} + +.dummy-data-table-container { + height: 10rem; + overflow-y: auto; +} + +.dummy-data-table { + margin-left: auto; + margin-right: auto; + border: 1px solid #444c50; +} + +.dummy-data-table th, .dummy-data-table tr { + padding: 0.5rem; + border-bottom: 1px solid #444c50; +} + +.dummy-table-row td { + padding: 0.5rem; + text-align: center; +} + +#generate-dummy-data { + padding: 0.2rem; + margin: 0.5rem; +} + +#label-option { + min-width: 60%; +} + +.query-label { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; +} + +#track { + min-width: 10px; + margin-left: 0.5rem; + width: 1.2rem; + height: 1.2rem; +} + +#chart-option { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: distribute; + justify-content: space-around; +} + #data-table { overflow: auto; - height: 300px; + height: 25em; + max-width: 90%; } .query-data { width: 1000px; } +.codemirror { + max-width: 90%; +} + +.CodeMirror-scroll { + overflow-x: scroll; +} + +.ReactCodeMirror { + max-width: 50rem; +} + #codemirror { padding: 15px 0; + max-width: 30rem; } #data-table::-webkit-scrollbar-track { @@ -129,7 +235,8 @@ input *:focus { /* Track */ ::-webkit-scrollbar-track { - box-shadow: inset 0px 0px 5px grey; + -webkit-box-shadow: inset 0px 0px 5px grey; + box-shadow: inset 0px 0px 5px grey; border-radius: 10px; } @@ -138,3 +245,10 @@ input *:focus { background: #c6d2d5; border-radius: 15px; } + +.DD-Dropdown { + max-height: 15rem; + min-height: 10rem; + min-width: 8rem; + overflow-y: scroll; +} diff --git a/frontend/assets/stylesheets/css/layout.css b/frontend/assets/stylesheets/css/layout.css index 2f8f0ef8..a5a6b687 100644 --- a/frontend/assets/stylesheets/css/layout.css +++ b/frontend/assets/stylesheets/css/layout.css @@ -1,39 +1,62 @@ @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); #splash-page { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex: 1; - flex-direction: column; - justify-content: center; - align-items: center; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; height: 100vh; color: #c6d2d5; } #splash-page .splash-prompt { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; margin-top: 30px; text-align: center; } #splash-page .splash-buttons { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; margin-top: 50px; + -ms-flex-pack: distribute; + justify-content: space-around; } #splash-page .splash-buttons button { border: 0.5px #444c50 solid; background-color: #596368; border-radius: 3px; - padding: 10px; + padding: 10px 25px 10px 25px; border: none; font-size: 1em; font-weight: bold; color: #2b2d35; outline: none; - margin: 0 5px; + margin-top: 10px; } #splash-page .splash-buttons button:hover { @@ -52,9 +75,42 @@ height: 362px; } +#custom-schema { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 25px; +} + +#import-schema { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 25px; +} + #main-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; height: 100vh; overflow: hidden; background-image: url("../../images/logo_monochrome.png"); @@ -64,83 +120,150 @@ } #main-left { - width: 34%; + width: 50%; + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; padding: 15px; background-color: #292a30; } #history-panel { height: 250px; + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; } .history-container { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; height: 250px; overflow-y: auto; } #compare-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-grow: 1; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; } #main-right { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; - flex-grow: 1; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; height: 100%; } #test-panels { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; height: 100%; } #schema-left { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; width: 50%; - flex: 1; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; padding: 15px; border-right: 0.5px solid #444c50; } #query-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; height: 50%; z-index: 1000; } #data-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; - flex-grow: 1; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; } #schema-right { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; width: 50%; padding: 15px; height: 100%; } #results-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-grow: 1; - flex-direction: column; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; } .results-container { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; height: 300px; overflow-y: auto; } diff --git a/frontend/assets/stylesheets/css/modal.css b/frontend/assets/stylesheets/css/modal.css index e915bf05..8ce45700 100644 --- a/frontend/assets/stylesheets/css/modal.css +++ b/frontend/assets/stylesheets/css/modal.css @@ -1,14 +1,18 @@ @import url("https://fonts.googleapis.com/css2?family=PT+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=PT+Mono&display=swap"); .modal { - width: 615px; - height: 700px; + width: 350px; + height: 400px; background-color: #30353a; border: 0.5px solid #1a1a1a; + -webkit-transition: 1.1s ease-out; transition: 1.1s ease-out; - box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); - filter: blur(0); - transform: scale(1); + -webkit-box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); + box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); + -webkit-filter: blur(0); + filter: blur(0); + -webkit-transform: scale(1); + transform: scale(1); opacity: 1; visibility: visible; padding: 40px; @@ -25,9 +29,12 @@ .modal.off { opacity: 0; visibility: hidden; - filter: blur(8px); - transform: scale(0.33); - box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); + -webkit-filter: blur(8px); + filter: blur(8px); + -webkit-transform: scale(0.33); + transform: scale(0.33); + -webkit-box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); + box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); } .modal h2 { @@ -53,10 +60,124 @@ } .modal-buttons { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; } -.modal-buttons .input-button { - margin-left: 15px; +#horizontal { + height: 1px; + background-color: #ccc; +} + +.load-schema { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: baseline; + -ms-flex-align: baseline; + align-items: baseline; +} + +#load-button { + margin-left: 20px; +} + +.separator { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + text-align: center; +} + +.separator::before, .separator::after { + content: ''; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + border-bottom: 1px solid #ccc; +} + +.separator::before { + margin-right: .25em; +} + +.separator::after { + margin-left: .25em; +} + +.copy-instance { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: baseline; + -ms-flex-align: baseline; + align-items: baseline; +} + +#select-dropdown { + margin-left: 20px; +} + +#copy-data-checkbox { + min-width: 10px; + margin-left: 0.5rem; + width: 1.2rem; + height: 1.2rem; +} + +.data-checkbox { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +#copy-button { + margin-top: 20px; +} + +#loading-modal { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: 100px; + height: 50px; + position: absolute; + top: 50%; + right: 50%; + bottom: 50%; + left: 50%; + z-index: 1020; } diff --git a/frontend/assets/stylesheets/css/style.css b/frontend/assets/stylesheets/css/style.css index c2f2c89b..2cf60152 100644 --- a/frontend/assets/stylesheets/css/style.css +++ b/frontend/assets/stylesheets/css/style.css @@ -32,6 +32,10 @@ background-color: #c6d2d5; } +#query-panel { + max-width: 90%; +} + #query-panel button { border: 0.5px #444c50 solid; background-color: #596368; @@ -95,7 +99,7 @@ input { background-color: #444c50; border: none; padding: 7px; - min-width: 240px; + min-width: 20em; outline: none; } @@ -103,17 +107,119 @@ input *:focus { outline: none; } +.dummy-data-select { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + padding: 1rem; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +#dummy-rows-input { + min-width: 10em; + margin-left: 1.5em; + margin-right: 1.5em; +} + +.dummy-data-table-container { + height: 10rem; + overflow-y: auto; +} + +.dummy-data-table { + margin-left: auto; + margin-right: auto; + border: 1px solid #444c50; +} + +.dummy-data-table th, .dummy-data-table tr { + padding: 0.5rem; + border-bottom: 1px solid #444c50; +} + +.dummy-table-row td { + padding: 0.5rem; + text-align: center; +} + +#generate-dummy-data { + padding: 0.2rem; + margin: 0.5rem; +} + +#label-option { + min-width: 60%; +} + +.query-label { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; +} + +#track { + min-width: 10px; + margin-left: 0.5rem; + width: 1.2rem; + height: 1.2rem; +} + +#chart-option { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -ms-flex-pack: distribute; + justify-content: space-around; +} + #data-table { overflow: auto; - height: 300px; + height: 25em; + max-width: 90%; } .query-data { width: 1000px; } +.codemirror { + max-width: 90%; +} + +.CodeMirror-scroll { + overflow-x: scroll; +} + +.ReactCodeMirror { + max-width: 50rem; +} + #codemirror { padding: 15px 0; + max-width: 30rem; } #data-table::-webkit-scrollbar-track { @@ -135,7 +241,8 @@ input *:focus { /* Track */ ::-webkit-scrollbar-track { - box-shadow: inset 0px 0px 5px grey; + -webkit-box-shadow: inset 0px 0px 5px grey; + box-shadow: inset 0px 0px 5px grey; border-radius: 10px; } @@ -145,40 +252,70 @@ input *:focus { border-radius: 15px; } +.DD-Dropdown { + max-height: 15rem; + min-height: 10rem; + min-width: 8rem; + overflow-y: scroll; +} + #splash-page { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex: 1; - flex-direction: column; - justify-content: center; - align-items: center; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; height: 100vh; color: #c6d2d5; } #splash-page .splash-prompt { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; margin-top: 30px; text-align: center; } #splash-page .splash-buttons { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; margin-top: 50px; + -ms-flex-pack: distribute; + justify-content: space-around; } #splash-page .splash-buttons button { border: 0.5px #444c50 solid; background-color: #596368; border-radius: 3px; - padding: 10px; + padding: 10px 25px 10px 25px; border: none; font-size: 1em; font-weight: bold; color: #2b2d35; outline: none; - margin: 0 5px; + margin-top: 10px; } #splash-page .splash-buttons button:hover { @@ -197,9 +334,42 @@ input *:focus { height: 362px; } +#custom-schema { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 25px; +} + +#import-schema { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 25px; +} + #main-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; height: 100vh; overflow: hidden; background-image: url("../../images/logo_monochrome.png"); @@ -209,96 +379,167 @@ input *:focus { } #main-left { - width: 34%; + width: 50%; + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; padding: 15px; background-color: #292a30; } #history-panel { height: 250px; + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; } .history-container { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; height: 250px; overflow-y: auto; } #compare-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-grow: 1; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; } #main-right { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; - flex-grow: 1; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; height: 100%; } #test-panels { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: row; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; height: 100%; } #schema-left { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; width: 50%; - flex: 1; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; padding: 15px; border-right: 0.5px solid #444c50; } #query-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; height: 50%; z-index: 1000; } #data-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; - flex-grow: 1; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; } #schema-right { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; width: 50%; padding: 15px; height: 100%; } #results-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-grow: 1; - flex-direction: column; + -webkit-box-flex: 1; + -ms-flex-positive: 1; + flex-grow: 1; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; } .results-container { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: column; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; height: 300px; overflow-y: auto; } .modal { - width: 615px; - height: 700px; + width: 350px; + height: 400px; background-color: #30353a; border: 0.5px solid #1a1a1a; + -webkit-transition: 1.1s ease-out; transition: 1.1s ease-out; - box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); - filter: blur(0); - transform: scale(1); + -webkit-box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); + box-shadow: -1rem 1rem 1rem rgba(0, 0, 0, 0.2); + -webkit-filter: blur(0); + filter: blur(0); + -webkit-transform: scale(1); + transform: scale(1); opacity: 1; visibility: visible; padding: 40px; @@ -315,9 +556,12 @@ input *:focus { .modal.off { opacity: 0; visibility: hidden; - filter: blur(8px); - transform: scale(0.33); - box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); + -webkit-filter: blur(8px); + filter: blur(8px); + -webkit-transform: scale(0.33); + transform: scale(0.33); + -webkit-box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); + box-shadow: 1rem 0 0 rgba(0, 0, 0, 0.2); } .modal h2 { @@ -343,12 +587,126 @@ input *:focus { } .modal-buttons { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; +} + +#horizontal { + height: 1px; + background-color: #ccc; +} + +.load-schema { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: baseline; + -ms-flex-align: baseline; + align-items: baseline; +} + +#load-button { + margin-left: 20px; +} + +.separator { + display: -webkit-box; + display: -ms-flexbox; display: flex; - flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + text-align: center; +} + +.separator::before, .separator::after { + content: ''; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + border-bottom: 1px solid #ccc; +} + +.separator::before { + margin-right: .25em; +} + +.separator::after { + margin-left: .25em; +} + +.copy-instance { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: baseline; + -ms-flex-align: baseline; + align-items: baseline; } -.modal-buttons .input-button { - margin-left: 15px; +#select-dropdown { + margin-left: 20px; +} + +#copy-data-checkbox { + min-width: 10px; + margin-left: 0.5rem; + width: 1.2rem; + height: 1.2rem; +} + +.data-checkbox { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +#copy-button { + margin-top: 20px; +} + +#loading-modal { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -ms-flex-pack: center; + justify-content: center; + width: 100px; + height: 50px; + position: absolute; + top: 50%; + right: 50%; + bottom: 50%; + left: 50%; + z-index: 1020; } * { @@ -387,13 +745,23 @@ h4 { } #compare-panel { + display: -webkit-box; + display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; flex-direction: column; margin-top: 2rem; } .compare-container { + display: -webkit-box; + display: -ms-flexbox; display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; flex-direction: column; overflow-y: auto; height: 200px; @@ -420,7 +788,8 @@ h4 { font-size: 0.8em; outline: none; color: #c6d2d5; - box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; background-repeat: no-repeat; border: none; cursor: pointer; @@ -436,7 +805,8 @@ h4 { width: 100px; color: #c6d2d5; display: block; - align-content: center; + -ms-flex-line-pack: center; + align-content: center; padding: 10px; text-decoration: none; font-family: 'PT Mono', monospace; @@ -460,6 +830,8 @@ h4 { .tab-list { border-bottom: 2px solid #c6d2d5; margin-right: 10px; + display: -webkit-box; + display: -ms-flexbox; display: flex; } diff --git a/frontend/assets/stylesheets/scss/components.scss b/frontend/assets/stylesheets/scss/components.scss index 10681094..1e4eab8a 100644 --- a/frontend/assets/stylesheets/scss/components.scss +++ b/frontend/assets/stylesheets/scss/components.scss @@ -30,6 +30,7 @@ } #query-panel { + max-width: 90%; button { border: 0.5px $border-darkmode solid; background-color: $button-darkmode; @@ -94,24 +95,107 @@ input { background-color: $border-darkmode; border: none; padding: 7px; - min-width: 240px; + min-width: 20em; outline: none; *:focus { outline: none; } } +.dummy-data-select { + display: flex; + flex-direction: row; + padding: 1rem; + align-items: center; +} + +#dummy-rows-input { + min-width: 10em; + margin-left: 1.5em; + margin-right: 1.5em; +} + +.dummy-data-table-container { + // display: flex; + // flex-direction: row; + // align-items: center; + // justify-content: center; + height: 10rem; + overflow-y: auto; +} + +.dummy-data-table { + margin-left: auto; + margin-right: auto; + border: 1px solid $border-darkmode; + th, tr { + padding: 0.5rem; + border-bottom: 1px solid $border-darkmode; + } +} + +.dummy-table-row { + td { + padding: 0.5rem; + text-align: center; + } +} + +#generate-dummy-data { + padding: 0.2rem; + margin: 0.5rem; +} + +#label-option { + min-width: 60%; +} + +.query-label { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +#track { + min-width: 10px; + margin-left: 0.5rem; + width: 1.2rem; + height: 1.2rem; +} + +#chart-option { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-around; +} + #data-table { overflow: auto; - height: 300px; + height: 25em; + max-width: 90%; } .query-data { width: 1000px; } +.codemirror { + max-width: 90%; +} + +.CodeMirror-scroll { + overflow-x: scroll; +} + +.ReactCodeMirror { + max-width: 50rem; +} + #codemirror { padding: 15px 0; + max-width: 30rem; } #data-table::-webkit-scrollbar-track { @@ -145,3 +229,9 @@ input { border-radius: 15px; } +.DD-Dropdown { + max-height: 15rem; + min-height: 10rem; + min-width: 8rem; + overflow-y: scroll; +} \ No newline at end of file diff --git a/frontend/assets/stylesheets/scss/layout.scss b/frontend/assets/stylesheets/scss/layout.scss index f9431c30..98a8db19 100644 --- a/frontend/assets/stylesheets/scss/layout.scss +++ b/frontend/assets/stylesheets/scss/layout.scss @@ -19,17 +19,19 @@ display: flex; flex-direction: row; margin-top: 50px; + justify-content: space-around; button { border: 0.5px $border-darkmode solid; background-color: $button-darkmode; border-radius: 3px; - padding: 10px; + padding: 10px 25px 10px 25px; border: none; font-size: 1em; font-weight: bold; color: $background-darkmode; outline: none; - margin: 0 5px; + // margin: 0 5px; + margin-top: 10px; } button:hover { background-color: $mint-green; @@ -47,6 +49,20 @@ } } +#custom-schema { + display: flex; + flex-direction: column; + align-items: center; + margin: 25px; +} + +#import-schema { + display: flex; + flex-direction: column; + align-items: center; + margin: 25px; +} + #main-panel { display: flex; flex-direction: row; @@ -59,7 +75,7 @@ } #main-left { - width: 34%; + width: 50%; display: flex; flex-direction: column; padding: 15px; diff --git a/frontend/assets/stylesheets/scss/modal.scss b/frontend/assets/stylesheets/scss/modal.scss index 66193f19..3d679762 100644 --- a/frontend/assets/stylesheets/scss/modal.scss +++ b/frontend/assets/stylesheets/scss/modal.scss @@ -1,7 +1,7 @@ @import './variables.scss'; .modal { - width: 615px; - height: 700px; + width: 350px; + height: 400px; background-color: $background-modal-darkmode; border: 0.5px solid $primary-color-lightmode; transition: 1.1s ease-out; @@ -57,9 +57,81 @@ width: 600px; } .modal-buttons { + display: flex; + flex-direction: row; +} + +#horizontal { + height: 1px; + background-color: #ccc; +} + +.load-schema { display: flex; flex-direction: row; - .input-button { - margin-left: 15px; - } + align-items: baseline; + +} + +#load-button { + margin-left: 20px; +} + +.separator { + display: flex; + align-items: center; + text-align: center; +} +.separator::before, .separator::after { + content: ''; + flex: 1; + border-bottom: 1px solid #ccc; +} +.separator::before { + margin-right: .25em; +} +.separator::after { + margin-left: .25em; } + +.copy-instance { + display: flex; + flex-direction: row; + align-items: baseline; +} + +#select-dropdown { + margin-left: 20px; +} + +#copy-data-checkbox { + min-width: 10px; + margin-left: 0.5rem; + width: 1.2rem; + height: 1.2rem; +} + +.data-checkbox { + display: flex; + flex-direction: row; + align-items: center; +} + +#copy-button { + margin-top: 20px; +} + +#loading-modal { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100px; + height: 50px; + position: absolute; + top: 50%; + right: 50%; + bottom: 50%; + left: 50%; + z-index: 1020; +} \ No newline at end of file diff --git a/frontend/components/App.tsx b/frontend/components/App.tsx index 873d26d2..d8f6f730 100644 --- a/frontend/components/App.tsx +++ b/frontend/components/App.tsx @@ -20,6 +20,7 @@ export class App extends Component { this.handleSkipClick = this.handleSkipClick.bind(this); } + // Splash page will always render upon opening App state: state = { openSplash: true, }; @@ -34,9 +35,9 @@ export class App extends Component { }, ) .then((result: object) => { - const filePathArr = result["filePaths"]; + const filePathArr = result['filePaths']; // send via channel to main process - if (!result["canceled"]) { + if (!result['canceled']) { ipcRenderer.send('upload-file', filePathArr); this.setState({ openSplash: false }); } @@ -46,6 +47,7 @@ export class App extends Component { }); } + // Skips file upload and moves to main page. handleSkipClick(event: ClickEvent) { ipcRenderer.send('skip-file-upload'); this.setState({ openSplash: false }); diff --git a/frontend/components/LoadingModal.tsx b/frontend/components/LoadingModal.tsx new file mode 100644 index 00000000..20d78148 --- /dev/null +++ b/frontend/components/LoadingModal.tsx @@ -0,0 +1,17 @@ +import React, { Component } from 'react'; +import ReactLoading from 'react-loading'; + +// "Loading" pop up renders whenever async functions are called +const LoadingModal = (props) => { + if (props.show) { + return( +
+

LOADING...

+ +
+ ); + } + else return null; +} + +export default LoadingModal; \ No newline at end of file diff --git a/frontend/components/MainPanel.tsx b/frontend/components/MainPanel.tsx index b9fe3e2a..75e57a65 100644 --- a/frontend/components/MainPanel.tsx +++ b/frontend/components/MainPanel.tsx @@ -1,7 +1,9 @@ +import { dialog } from 'electron'; import React, { Component } from 'react'; import { Compare } from './leftPanel/Compare'; import History from './leftPanel/History'; import { Tabs } from './rightPanel/Tabs'; +import LoadingModal from './LoadingModal'; const { ipcRenderer } = window.require('electron'); @@ -15,6 +17,7 @@ type MainState = { }[]; currentSchema: string; lists: any; + loading: boolean; }; type MainProps = {}; @@ -30,12 +33,13 @@ class MainPanel extends Component { lists: { databaseList: ['defaultDB'], tableList: [], - } + }, + loading: false }; componentDidMount() { ipcRenderer.send('return-db-list'); - + // Listening for returnedData from executing Query // Update state with new object (containing query data, query statistics, query schema // inside of state.queries array @@ -58,26 +62,48 @@ class MainPanel extends Component { }); ipcRenderer.on('db-lists', (event: any, returnedLists: any) => { - this.setState({ lists: returnedLists }) - this.onClickTabItem(this.state.lists.databaseList[this.state.lists.databaseList.length - 1]) - }) + this.setState(prevState => ({ + ...prevState, + lists: { + databaseList: returnedLists.databaseList, + tableList: returnedLists.tableList + } + })) + }); + + ipcRenderer.on('switch-to-new', (event: any) => { + const newSchemaIndex = this.state.lists.databaseList.length - 1; + this.setState({currentSchema: this.state.lists.databaseList[newSchemaIndex]}); + }); + + // Renders the loading modal during async functions. + ipcRenderer.on('async-started', (event: any) => { + this.setState({ loading: true }); + }); + + ipcRenderer.on('async-complete', (event: any) => { + this.setState({ loading: false }); + }); } onClickTabItem(tabName) { ipcRenderer.send('change-db', tabName); - ipcRenderer.on('return-change-db', (event: any, db_name: string) => { - this.setState({ currentSchema: tabName }); - }); + ipcRenderer.send('return-db-list'); + this.setState({ currentSchema: tabName }); } render() { + return (
+
+ +
- +
); } diff --git a/frontend/components/Splash.tsx b/frontend/components/Splash.tsx index 3ac33313..72fae97d 100644 --- a/frontend/components/Splash.tsx +++ b/frontend/components/Splash.tsx @@ -16,14 +16,19 @@ export class Splash extends Component { return (
-
+

Welcome!

-

Import database in .sql or .tar?

-
-
- - -
+
+
+

Create custom schema

+ +
+
+

Import database in .sql or .tar

+ +
+
+
); } diff --git a/frontend/components/leftPanel/Compare.tsx b/frontend/components/leftPanel/Compare.tsx index 0965920e..0c5607dc 100644 --- a/frontend/components/leftPanel/Compare.tsx +++ b/frontend/components/leftPanel/Compare.tsx @@ -17,31 +17,51 @@ type CompareProps = { }; export const Compare = (props: CompareProps) => { - let initial: any = { ...props, compareList: [] }; + // ------------------------------------------------------------------------------------------------------------- + // ------------------------------------ logic for setting state -------------------------------------------- + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + + // declaring initial state + let initial: any = { ...props, compareList: [] }; const [queryInfo, setCompare] = useState(initial); + const addCompareQuery = (event) => { + // compare list is a dropdown menu on the front-end let compareList = queryInfo.compareList; props.queries.forEach((query) => { + // if the query is clicked in the dropdown menu if (query.queryLabel === event.target.text) { - compareList.push(query); + // only allow the addition of queries that aren't already being compared + if (!compareList.includes(query)){ + compareList.push(query); + } } }); + // reset state to account for the change in queries being tracked setCompare({ ...queryInfo, compareList }); } + // ------------------------------------------------------------------------------------------------------------- + // ------------------------------------ logic for the compare query table -------------------------------------- + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + const deleteCompareQuery = (event) => { + // reset comparelist so that the query that is chosen is not included any more let compareList: any = queryInfo.compareList.filter( (query) => query.queryLabel !== event.target.id); setCompare({ ...queryInfo, compareList }); } const dropDownList = () => { + // for each query on the query list, make a dropdown item in the menu return props.queries.map((query, index) => {query.queryLabel}); }; + // Rendering the compare table with selected queries from dropdown list const renderCompare = () => { return queryInfo.compareList.map((query, index) => { + // destructuring data and variables from queries on the compare list const { queryString, queryData, queryStatistics, querySchema, queryLabel } = query; const { ['QUERY PLAN']: queryPlan } = queryStatistics[0]; const { @@ -78,22 +98,78 @@ export const Compare = (props: CompareProps) => { }); }; - const { compareList } = queryInfo; - const labelData = () => compareList.map((query) => query.queryLabel); - const runtimeData = () => compareList.map( - (query) => query.queryStatistics[0]["QUERY PLAN"][0]["Execution Time"] + query.queryStatistics[0]["QUERY PLAN"][0]["Planning Time"]); - const data = { - labels: labelData(), - datasets: [ - { - label: 'Runtime', - backgroundColor: 'rgb(108, 187, 169)', - borderColor: 'rgba(247,247,247,247)', - borderWidth: 2, - data: runtimeData(), + // ------------------------------------------------------------------------------------------------------------- + // ------------------------------------ logic for the compare query graph -------------------------------------- + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + + const generateDatasets = () => { + const { compareList } = queryInfo; + + // first we create an object with all of the comparelist data organized in a way that enables us to render our graph easily + const compareDataObject: any = {}; + // then we populate that object + for (const query of compareList){ + const { queryLabel, querySchema, queryStatistics } = query; + if (!compareDataObject[querySchema]){ + compareDataObject[querySchema] = { + [queryLabel.toString()] : queryStatistics[0]["QUERY PLAN"][0]["Execution Time"] + queryStatistics[0]["QUERY PLAN"][0]["Planning Time"] + } + } else { + compareDataObject[querySchema][queryLabel.toString()] = queryStatistics[0]["QUERY PLAN"][0]["Execution Time"] + queryStatistics[0]["QUERY PLAN"][0]["Planning Time"] } - ] - }; + }; + + // then we generate a labelData array to store all unique query labels + const labelDataArray: any = []; + for (const schema in compareDataObject){ + for (const label in compareDataObject[schema]) { + if (!labelDataArray.includes(label)){ + labelDataArray.push(label); + } + } + } + + + // then we generate an array of data for each schema, storing data for each unique query according to the schema + const runTimeDataArray: any = []; + for (const schema in compareDataObject){ + const schemaArray: any = []; + for(const label of labelDataArray){ + schemaArray.push(compareDataObject[schema][label] ? compareDataObject[schema][label] : 0) + } + runTimeDataArray.push({[schema]: schemaArray}); + } + + // creating a list of possible colors for the graph + const schemaColors = { + nextColor: 0, + colorList: ['#006C67', '#F194B4', '#FFB100', '#FFEBC6', '#A4036F', '#048BA8', '#16DB93', '#EFEA5A', '#F29E4C'] + } + + // then we generate datasets for each schema for the bar chart + const datasets = runTimeDataArray.map((schemaDataObject) => { + const schemaLabel: any = Object.keys(schemaDataObject)[0]; + const color = schemaColors.colorList[schemaColors.nextColor % schemaColors.colorList.length]; + schemaColors.nextColor += 1; + return { + label: `${schemaLabel}`, + backgroundColor: color, + borderColor: color, + borderWidth: 1, + data: schemaDataObject[schemaLabel] + } + }) + + //then we combine the label array and the data arrays for each schema into a data object to pass to our bar graph + return { + labels: labelDataArray, + datasets: datasets + } + } + + // ------------------------------------------------------------------------------------------------------------- + // ------------------------------------ rendering the elements ------------------------------------------------- + // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv return (
@@ -121,7 +197,7 @@ export const Compare = (props: CompareProps) => {
{ fontSize: 16 }, legend: { - display: false, + display: true, position: 'right' - } + }, + maintainAspectRatio: false }} />
diff --git a/frontend/components/leftPanel/History.tsx b/frontend/components/leftPanel/History.tsx index fab4f85b..2506667c 100644 --- a/frontend/components/leftPanel/History.tsx +++ b/frontend/components/leftPanel/History.tsx @@ -11,6 +11,7 @@ type HistoryProps = { currentSchema: string; }; +// Top left panel component displaying previously run queries export class History extends Component { constructor(props: HistoryProps) { super(props); diff --git a/frontend/components/rightPanel/SchemaContainer.tsx b/frontend/components/rightPanel/SchemaContainer.tsx index 4864676c..7495d1d3 100644 --- a/frontend/components/rightPanel/SchemaContainer.tsx +++ b/frontend/components/rightPanel/SchemaContainer.tsx @@ -1,11 +1,11 @@ import React, { Component } from 'react'; import { Data } from './schemaChildren/Data'; -import { Results } from './schemaChildren/Results'; import Query from './schemaChildren/Query'; type SchemaContainerProps = { queries: any; currentSchema: string; + tableList: string[]; }; type state = { @@ -26,11 +26,12 @@ export class SchemaContainer extends Component {
- - -
-
- +
+ +
+
+ +
diff --git a/frontend/components/rightPanel/Tabs.tsx b/frontend/components/rightPanel/Tabs.tsx index f7e66d47..6573120b 100644 --- a/frontend/components/rightPanel/Tabs.tsx +++ b/frontend/components/rightPanel/Tabs.tsx @@ -10,6 +10,7 @@ type TabsProps = { tabList: string[], queries: any, onClickTabItem: any, + tableList: string[] } type state = { @@ -21,7 +22,7 @@ export class Tabs extends Component { this.showModal = this.showModal.bind(this); } state: state = { - show: false, + show: false }; showModal = (event: any) => { @@ -35,6 +36,7 @@ export class Tabs extends Component { // thing as all the databases). We open a channel to listen for it here inside of componendDidMount, then // we invoke onClose to close schemaModal ONLY after we are sure that backend has created that channel. ipcRenderer.on('db-lists', (event: any, returnedLists: any) => { + this.setState({currentSchema: returnedLists}) this.onClose(event); }) } @@ -79,11 +81,11 @@ export class Tabs extends Component { - +
{tabList.map((tab, index) => { if (tab !== currentSchema) return undefined; - return ; + return ; })}
diff --git a/frontend/components/rightPanel/schemaChildren/Data.tsx b/frontend/components/rightPanel/schemaChildren/Data.tsx index 6ad5986e..75cad1b6 100644 --- a/frontend/components/rightPanel/schemaChildren/Data.tsx +++ b/frontend/components/rightPanel/schemaChildren/Data.tsx @@ -16,15 +16,12 @@ export class Data extends Component { super(props); } + // Rendering results of tracked query from Query panel. render() { const { queries } = this.props; return (
-
-
-
-

Data Table

{queries.length === 0 ? null :
} diff --git a/frontend/components/rightPanel/schemaChildren/DummyDataPanel.tsx b/frontend/components/rightPanel/schemaChildren/DummyDataPanel.tsx new file mode 100644 index 00000000..5402c971 --- /dev/null +++ b/frontend/components/rightPanel/schemaChildren/DummyDataPanel.tsx @@ -0,0 +1,201 @@ +import React, { Component } from 'react'; +import DropdownButton from 'react-bootstrap/DropdownButton'; +import Dropdown from 'react-bootstrap/Dropdown'; + +const { dialog } = require('electron').remote; +const { ipcRenderer } = window.require('electron'); + +type ClickEvent = React.MouseEvent; + +type DummyDataPanelProps = { + currentSchema: string; + tableList: string[]; +}; + +type state = { + currentTable: string, + dataInfo: {}, + rowNumber: string +} + +class DummyDataPanel extends Component { + + constructor(props: DummyDataPanelProps) { + super(props); + this.dropDownList = this.dropDownList.bind(this); + this.selectHandler = this.selectHandler.bind(this); + this.addToTable = this.addToTable.bind(this); + this.changeRowNumber = this.changeRowNumber.bind(this); + this.deleteRow = this.deleteRow.bind(this); + this.submitDummyData = this.submitDummyData.bind(this); + } + + state: state = { + currentTable: 'select table', + dataInfo: {}, + rowNumber: '' + } + + //handler to change the dropdown display to the selected table name + selectHandler = (eventKey, e: React.SyntheticEvent) => { + if (eventKey !== 'none') { + this.setState({currentTable: eventKey}); + } + }; + + //function to generate the dropdown optiosn from the table names in state + dropDownList = () => { + const result: any = []; + let tableName; + // Checks to make sure tables are available to generate dummy data to. + // Allows user to choose a specific table, or to write dummy data to all tables. + if (this.props.tableList.length > 0) { + for (let i = 0; i <= this.props.tableList.length; i++) { + if(this.props.tableList[i]) tableName = this.props.tableList[i]; + else tableName = 'all'; + result.push({tableName}); + } + } else { + // Adds message in dropdown list to show that not tables are available + // Went this route because we couldn't get the dropdown to disappear if there were no tables in tableList + result.push(No tables available!); + } + return result; + }; + + //submit listener to add table name and rows to the dataInfo object in state + addToTable = (event: any) => { + event.preventDefault(); + //if no number is entered + if (!this.state.rowNumber) { + dialog.showErrorBox('Please enter a number of rows.', ''); + } + if (this.state.currentTable === 'select table') { + dialog.showErrorBox('Please select a table.', ''); + } + //reset input fields and update nested object in state + else { + let table = this.state.currentTable; + let number = Number(this.state.rowNumber); + if (table !== 'all') { + this.setState(prevState => ({ + ...prevState, + currentTable: 'select table', + rowNumber: '', + dataInfo: { + ...prevState.dataInfo, + [table]: number + } + })) + } + else { + const dataInfo = {}; + this.props.tableList.forEach(table => { + if (table !== 'all') { + dataInfo[table] = number; + } + }) + this.setState(prevState => ({ + ...prevState, + currentTable: 'select table', + rowNumber: '', + dataInfo + })) + } + } + } + + //onclick listener to delete row from table + deleteRow = (event: any) => { + let name = event.target.id; + this.setState(prevState => ({ + ...prevState, + dataInfo: { + ...prevState.dataInfo, + [name]: undefined + } + })) + } + + //onchange listener to update the rowNumber string in state + changeRowNumber = (event: any) => { + this.setState({ rowNumber: event.target.value }) + } + + createRow = () => { + //once state updates on click, render the table row from the object + const newRows: JSX.Element[] = []; + for (let key in this.state.dataInfo) { + if (this.state.dataInfo[key]) { + newRows.push( + + + + + + ) + } + } + return newRows; + } + + submitDummyData = (event: any) => { + //check if there are requested dummy data values + if (Object.keys(this.state.dataInfo).length) { + //creates a dummyDataRequest object with schema name and table name/rows + const dummyDataRequest = { + schemaName: this.props.currentSchema, + dummyData: this.state.dataInfo + } + ipcRenderer.send('generate-dummy-data', dummyDataRequest); + //reset state to clear the dummy data panel's table + this.setState({dataInfo: {}}); + } + else dialog.showErrorBox('Please add table and row numbers', ''); + } + + render() { + + return ( +
+

Generate Dummy Data

+

Select table and number of rows:

+
+ + + {this.state.currentTable} + + + {this.dropDownList()} + + + + + +
+
+
{key}{this.state.dataInfo[key]}
+ + + + + + + {this.createRow()} + +
table# of rowsdelete
+ +
+ +
+ + ) + } +} + +export default DummyDataPanel; \ No newline at end of file diff --git a/frontend/components/rightPanel/schemaChildren/Query.tsx b/frontend/components/rightPanel/schemaChildren/Query.tsx index 67d9532a..18f24ce4 100644 --- a/frontend/components/rightPanel/schemaChildren/Query.tsx +++ b/frontend/components/rightPanel/schemaChildren/Query.tsx @@ -1,4 +1,6 @@ import React, { Component } from 'react'; +//delete before pull request +import DummyDataPanel from './DummyDataPanel'; const { ipcRenderer } = window.require('electron'); const { dialog } = require('electron').remote; @@ -7,18 +9,23 @@ const { dialog } = require('electron').remote; import 'codemirror/lib/codemirror.css'; // Styline import 'codemirror/mode/sql/sql'; // Language (Syntax Highlighting) import 'codemirror/theme/lesser-dark.css'; // Theme -import CodeMirror from 'react-codemirror'; +import CodeMirror from '@skidding/react-codemirror'; /************************************************************ *********************** TYPESCRIPT: TYPES *********************** ************************************************************/ -type QueryProps = { currentSchema: string }; +type QueryProps = { + currentSchema: string; + tableList: string[]; +}; type state = { queryString: string; queryLabel: string; show: boolean; + //if true, will add query results to the bar chart + trackQuery: boolean; }; class Query extends Component { @@ -26,21 +33,34 @@ class Query extends Component { super(props); this.handleQuerySubmit = this.handleQuerySubmit.bind(this); this.updateCode = this.updateCode.bind(this); - // this.handleQueryPrevious = this.handleQueryPrevious.bind(this); - // this.handleGenerateData = this.handleGenerateData.bind(this); + this.handleTrackQuery = this.handleTrackQuery.bind(this); } state: state = { queryString: '', queryLabel: '', show: false, + trackQuery: false }; + componentDidMount() { + ipcRenderer.on('query-error', (event: any, message: string) => { + console.log('Query error: '); + // dialog.showErrorBox('Error', message); + + }) + } + // Updates state.queryString as user inputs query label handleLabelEntry(event: any) { this.setState({ queryLabel: event.target.value }); } + // Updates state.trackQuery as user checks or unchecks box + handleTrackQuery(event: any) { + this.setState({ trackQuery: event.target.checked }); + } + // Updates state.queryString as user inputs query string updateCode(newQueryString: string) { this.setState({ @@ -51,24 +71,37 @@ class Query extends Component { // Submits query to backend on 'execute-query' channel handleQuerySubmit(event: any) { event.preventDefault(); - // if input fields for query label or query string are empty, then - // send alert to input both fields - if (!this.state.queryLabel || !this.state.queryString) { - dialog.showErrorBox('Please enter a Label and a Query.', ''); - } else { + // if query string is empty, show error + if (!this.state.queryString) { + dialog.showErrorBox('Please enter a Query.', ''); + } + if (!this.state.trackQuery) { + //functionality to send query but not return stats and track + const queryAndSchema = { + queryString: this.state.queryString, + queryCurrentSchema: this.props.currentSchema, + queryLabel: this.state.queryLabel, + }; + ipcRenderer.send('execute-query-untracked', queryAndSchema); + //reset frontend inputs to display as empty and unchecked + this.setState({ queryLabel: '', trackQuery: false, queryString: '' }); + } + if (this.state.trackQuery && !this.state.queryLabel) { + dialog.showErrorBox('Please enter a label for the Query.', ''); + } + else if (this.state.trackQuery) { + // send query and return stats from explain/analyze const queryAndSchema = { queryString: this.state.queryString, queryCurrentSchema: this.props.currentSchema, queryLabel: this.state.queryLabel, }; - ipcRenderer.send('execute-query', queryAndSchema); + ipcRenderer.send('execute-query-tracked', queryAndSchema); + //reset frontend inputs to display as empty and unchecked + this.setState({ queryLabel: '', trackQuery: false, queryString: '' }); } } - // handleGenerateData(event: any) { - // ipcRenderer.send('generate-data') - // } - render() { // Codemirror module configuration options var options = { @@ -79,31 +112,46 @@ class Query extends Component { return (
+
+ +

Query

- - this.handleLabelEntry(e)} - /> -
+
+
+ track on chart: + +
+
+ + this.handleLabelEntry(e)} + /> +
+

- + {/* */}


-

*required

- {/* */}
); } diff --git a/frontend/components/rightPanel/schemaChildren/Results.tsx b/frontend/components/rightPanel/schemaChildren/Results.tsx deleted file mode 100644 index 81fad2cf..00000000 --- a/frontend/components/rightPanel/schemaChildren/Results.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { Component } from 'react'; -import { Line, defaults } from "react-chartjs-2"; - -type ResultsProps = { - queries: { - queryString: string; - queryData: {}[]; - queryStatistics: any - querySchema: string; - queryLabel: string; - }[]; -}; - -defaults.global.defaultFontColor = 'rgb(198,210,213)'; - -export class Results extends Component { - constructor(props: ResultsProps) { - super(props); - } - renderTableData() { - - return this.props.queries.map((query, index) => { - // destructure state from mainPanel, including destructuring object returned from Postgres - const { queryString, queryData, queryStatistics, querySchema, queryLabel } = query; - const { ['QUERY PLAN']: queryPlan } = queryStatistics[0]; - const { - Plan, - ['Planning Time']: planningTime, - ['Execution Time']: executionTime, - } = queryPlan[0]; - const { - ['Node Type']: scanType, - ['Actual Rows']: actualRows, - ['Actual Startup Time']: actualStartupTime, - ['Actual Total Time']: actualTotalTime, - ['Actual Loops']: loops, - } = Plan; - const runtime = (planningTime + executionTime).toFixed(3); - return ( - - {queryLabel} - {queryString} - {/* {scanType} */} - {planningTime} - {runtime} - {/* {executionTime} - {actualStartupTime} */} - {/* {actualTotalTime} */} - {/* {actualRows} */} - {loops} - {/* {'Notes'} */} - - ); - }); - } - - render() { - const { queries } = this.props; - const labelData = () => queries.map((query) => query.queryLabel); - const runtimeData = () => queries.map( - (query) => query.queryStatistics[0]["QUERY PLAN"][0]["Execution Time"] + query.queryStatistics[0]["QUERY PLAN"][0]["Planning Time"]); - const data = { - labels: labelData(), - datasets: [ - { - label: 'Runtime', - fill: false, - lineTension: 0.5, - backgroundColor: 'rgb(108, 187, 169)', - borderColor: 'rgba(247,247,247,247)', - borderWidth: 2, - data: runtimeData(), - } - ] - } - - // To display additional analytics, comment back in JSX elements in the return statement below. - return ( -
-

Results

-
- - - - - - {/* */} - - - {/* - - */} - {/* */} - {/* */} - - {/* */} - - {this.renderTableData()} - -
{'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'}
-
-
- -
-
- ); - } -} diff --git a/frontend/components/rightPanel/schemaChildren/SchemaInput.tsx b/frontend/components/rightPanel/schemaChildren/SchemaInput.tsx deleted file mode 100644 index dd2e4ce1..00000000 --- a/frontend/components/rightPanel/schemaChildren/SchemaInput.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React, { Component } from 'react'; -// import GenerateData from './GenerateData'; - -const { ipcRenderer } = window.require('electron'); - -// Codemirror configuration -import 'codemirror/lib/codemirror.css'; // Styline -import 'codemirror/mode/sql/sql'; // Language (Syntax Highlighting) -import 'codemirror/theme/lesser-dark.css'; // Theme -import CodeMirror from 'react-codemirror'; - -type SchemaInputProps = { - onClose: any; - schemaName: string; -}; - -type state = { - schemaEntry: string; -}; - -class SchemaInput extends Component { - constructor(props: SchemaInputProps) { - super(props); - this.handleSchemaSubmit = this.handleSchemaSubmit.bind(this); - this.handleSchemaChange = this.handleSchemaChange.bind(this); - } - - state: state = { - schemaEntry: '', - }; - - // Updates state.schemaEntry as user inputs query string - handleSchemaChange(event: string) { - this.setState({ - schemaEntry: event, - }); - } - - handleSchemaSubmit(event: any) { - event.preventDefault(); - - const schemaObj = { - schemaName: this.props.schemaName, - schemaFilePath: '', - schemaEntry: this.state.schemaEntry, - }; - - ipcRenderer.send('input-schema', schemaObj); - } - - onClose = (event: any) => { - this.props.onClose && this.props.onClose(event); - }; - - render() { - // Codemirror module configuration options - var options = { - lineNumbers: true, - mode: 'sql', - theme: 'lesser-dark', - }; - - return ( -
-
-
-
- this.handleSchemaChange(e)} - options={options} - /> -
- -
-
- ); - } -} - -export default SchemaInput; diff --git a/frontend/components/rightPanel/schemaChildren/SchemaModal.tsx b/frontend/components/rightPanel/schemaChildren/SchemaModal.tsx index d9981418..bfe168e7 100644 --- a/frontend/components/rightPanel/schemaChildren/SchemaModal.tsx +++ b/frontend/components/rightPanel/schemaChildren/SchemaModal.tsx @@ -1,6 +1,5 @@ import React, { Component } from 'react'; -import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom'; -import SchemaInput from './SchemaInput'; +import { Dropdown } from 'react-bootstrap'; // import GenerateData from './GenerateData'; const { dialog } = require('electron').remote; @@ -9,6 +8,7 @@ const { ipcRenderer } = window.require('electron'); type ClickEvent = React.MouseEvent; type SchemaModalProps = { + tabList: string[]; show: boolean; showModal: any; onClose: any; @@ -19,6 +19,8 @@ type state = { schemaFilePath: string; schemaEntry: string; redirect: boolean; + dbCopyName: string; + copy: boolean }; class SchemaModal extends Component { @@ -28,6 +30,10 @@ class SchemaModal extends Component { this.handleSchemaFilePath = this.handleSchemaFilePath.bind(this); this.handleSchemaEntry = this.handleSchemaEntry.bind(this); this.handleSchemaName = this.handleSchemaName.bind(this); + this.selectHandler = this.selectHandler.bind(this); + this.handleCopyData = this.handleCopyData.bind(this); + this.dropDownList = this.dropDownList.bind(this); + this.handleCopyFilePath = this.handleCopyFilePath.bind(this); // this.handleQueryPrevious = this.handleQueryPrevious.bind(this); // this.handleQuerySubmit = this.handleQuerySubmit.bind(this); @@ -38,6 +44,8 @@ class SchemaModal extends Component { schemaFilePath: '', schemaEntry: '', redirect: false, + dbCopyName: 'Select Instance', + copy: false }; @@ -68,7 +76,11 @@ class SchemaModal extends Component { schemaFilePath: this.state.schemaFilePath, schemaEntry: '', }; - ipcRenderer.send('input-schema', schemaObj); + if (!result['canceled']) { + ipcRenderer.send('input-schema', schemaObj); + this.setState({ schemaName: ''}); + } + this.setState({ dbCopyName: 'Select Instance'}); this.props.showModal(event); }) .catch((err: object) => { @@ -94,6 +106,36 @@ class SchemaModal extends Component { ipcRenderer.send('input-schema', schemaObj); } + selectHandler = (eventKey, e: React.SyntheticEvent) => { + this.setState({ dbCopyName: eventKey }); + } + + handleCopyData(event: any) { + if(!this.state.copy) this.setState({ copy: true }); + else this.setState({ copy: false }); + } + + dropDownList = () => { + return this.props.tabList.map((db, index) => {db}); + }; + + handleCopyFilePath(event: any) { + event.preventDefault(); + + const schemaObj = { + schemaName: this.state.schemaName, + schemaFilePath: '', + schemaEntry: '', + dbCopyName: this.state.dbCopyName, + copy: this.state.copy + } + + ipcRenderer.send('input-schema', schemaObj); + this.setState({ dbCopyName: 'Select Instance'}); + this.setState({ schemaName: ''}); + this.props.showModal(event); + } + render() { if (this.props.show === false) { return null; @@ -101,38 +143,58 @@ class SchemaModal extends Component { return (