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/__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/channels.ts b/backend/channels.ts index 567ae7da..7b922635 100644 --- a/backend/channels.ts +++ b/backend/channels.ts @@ -1,8 +1,11 @@ // Import parts of electron to use -import { ipcMain } from 'electron'; +import { dialog, ipcMain } from 'electron'; +import { create } from 'domain'; +const { generateDummyData, writeCSVFile } = require('./newDummyD/dummyDataMain'); const { exec } = require('child_process'); const db = require('./models'); +const path = require('path'); /************************************************************ *********************** IPC CHANNELS *********************** @@ -20,8 +23,7 @@ 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); + db.changeDB(dbName) }); // Generate CLI commands to be executed in child process. @@ -45,15 +47,21 @@ const runTARFunc = (file) => { 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}`); if (nextStep) nextStep(); + //this shows the console error in an error message on the frontend + else dialog.showErrorBox('Success', ''); }); }; @@ -75,39 +83,71 @@ 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); }; - // 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. +// AND +// Listen for and handle DB copying events ipcMain.on('input-schema', (event, data: SchemaType) => { - const { schemaName: dbName, schemaFilePath: filePath, schemaEntry } = data; + + const { schemaName: dbName, schemaEntry, dbCopyName, copy } = data; + let { schemaFilePath: filePath } = data; + + 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 followed by pg_restore + + // reset file path such that it points to our newly created local .sql file + filePath = [path.join(__dirname, `./${dbName}.sql`)]; + + //Exact copy + if(copy) { + console.log('in copy if statement'); + execute(`docker exec postgres-1 pg_dump -U postgres ${dbCopyName} > tsCompiled/backend/${dbName}.sql`, null); + } + // Hollow copy + else execute(`docker exec postgres-1 pg_dump -s -U postgres ${dbCopyName} > tsCompiled/backend/${dbName}.sql`, null) + } + + console.log(dbName, schemaEntry, dbCopyName, copy, filePath); // 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. @@ -127,14 +167,14 @@ ipcMain.on('input-schema', (event, data: SchemaType) => { // 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); }; - // 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; @@ -142,8 +182,14 @@ ipcMain.on('input-schema', (event, data: SchemaType) => { 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 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); @@ -159,8 +205,19 @@ interface QueryType { queryStatistics: string; } +ipcMain.on('execute-query-untracked', (event, data: QueryType) => { + console.log('execute query untracked'); + // destructure object from frontend + const { queryString, queryCurrentSchema, queryLabel } = data; + // run query on db + db.query(queryString) + .catch((error: string) => { + console.log('ERROR in execute-query-untracked channel in main.ts', error); + }); +}); + // Listen for queries being sent from renderer -ipcMain.on('execute-query', (event, data: QueryType) => { +ipcMain.on('execute-query-tracked', (event, data: QueryType) => { // destructure object from frontend const { queryString, queryCurrentSchema, queryLabel } = data; @@ -192,8 +249,70 @@ ipcMain.on('execute-query', (event, data: QueryType) => { }); }) .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) => { + let schemaLayout; + let dummyDataRequest = data; + let tableMatricesArray; + db.getSchemaLayout() + .then((result) => { + schemaLayout = result; + }) + .then(() => { + // generate the dummy data and save it into matrices associated with table names + tableMatricesArray = generateDummyData(schemaLayout, dummyDataRequest); + }) + .then(() => { + let csvPromiseArray: any = []; + //iterate through tableMatricesArray to write individual .csv files + for (const tableObject of tableMatricesArray) { + // extract tableName from tableObject + let tableName: string = tableObject.tableName; + //mapping column headers from getColumnObjects in models.ts to columnNames + let columnArray: string[] = schemaLayout.tables[tableName].map(columnObj => columnObj.columnName) + //write all entries in tableMatrix to csv file + csvPromiseArray.push(writeCSVFile(tableObject.data, tableName, columnArray)); + } + Promise.all(csvPromiseArray) + .then(() => { + // let copyFilePromiseArray: any = []; + //iterate through tableMatricesArray to copy individual .csv files to the respective tables + for (const tableObject of tableMatricesArray) { + // extract tableName from tableObject + let tableName: string = tableObject.tableName; + // write filepath to created .csv files + let compiledPath: any = [path.join(__dirname, `./${tableName}.csv`)]; + + execute(importFileFunc(compiledPath), null); + + console.log('after first execute'); + if (process.platform === 'win32'){ + compiledPath = compiledPath.replace(/\\/g,`/`); + } + + let queryString: string = `COPY ${tableName} FROM '/data_dump' WITH CSV HEADER;`; + // let values: string[] = [tableName, compiledPath]; + + execute(`docker exec postgres-1 psql -U postgres -d ${data.schemaName} -c "${queryString}" `, null); + + // db.query(queryString) + // .catch((error: string) => { + // console.log('ERROR in dummy-generation channel in channels.ts', error); + // }); + // execute(`docker exec postgres-1 psql -U postgres -c "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;"`, null) + } + }) + }) + +}) + +export default execute; +// module.exports; diff --git a/backend/main.ts b/backend/main.ts index 58152cc1..fbdd8d6d 100644 --- a/backend/main.ts +++ b/backend/main.ts @@ -90,7 +90,9 @@ function createWindow() { // Emitted when the window is closed. mainWindow.on('closed', function () { // 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 pruneVolumes: string = 'docker volume prune -f'; //this will force remove ALL volumes in docker. Might not want to run it const executeQuery = (str) => { exec(str, (error, stdout, stderr) => { if (error) { @@ -104,11 +106,14 @@ function createWindow() { console.log(`${stdout}`); }) }; + executeQuery(stopContainers); executeQuery(pruneContainers); + // executeQuery(pruneVolumes); mainWindow = null; }); } + // Invoke createWindow to create browser windows after Electron has been initialized. // Some APIs can only be used after this event occurs. app.on('ready', createWindow); diff --git a/backend/models.ts b/backend/models.ts index f5c9a33f..5033b4e3 100644 --- a/backend/models.ts +++ b/backend/models.ts @@ -5,45 +5,124 @@ const { Pool } = require('pg'); let PG_URI: string = 'postgres://postgres:postgres@localhost:5432/defaultDB'; let pool: any = new Pool({ connectionString: PG_URI }); +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); + }) + }) +} + +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); + }) + }) +} + +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); + }) + }) + }, + + 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: [columnNames 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)) + } + Promise.all(promiseArray) + .then((columnInfo) => { + 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 + } +} \ No newline at end of file diff --git a/backend/newDummyD/dummyDataMain.ts b/backend/newDummyD/dummyDataMain.ts new file mode 100644 index 00000000..120a8809 --- /dev/null +++ b/backend/newDummyD/dummyDataMain.ts @@ -0,0 +1,197 @@ +//this file maps table names from the schemaLayout object to individual sql files for DD generation + +import faker from "faker"; +import execute from '../channels'; +import fs from 'fs'; +import { table } from "console"; +const path = require('path'); +const { exec } = require('child_process'); + +// 3. get schema layout +// const schemaLayout: any = { +// // tableNames: ['Johnny Bravo', 'Teen Titans', ...] +// tableNames: [], +// tables: { +// // tableName: [columnNames array] +// } +// }; +// 4. iterate over tables in hollow schema (Options: 1. run sql query to get list of tables; 2. use 'getList' function defined in models.ts) +// 5. generate new .sql file +// 6. create dummyData object, e.g.: +// const DD = { +// // people +// table_1: { +// // columnArray: ["_id", "name", "homeworld_id", ...] +// columnArray: [], +// // [[1,2,3,4,...],["luke", "leia", "jabaDaHut", ...], ...] +// tableMatrix: [[],[]] +// } +// } +// 7. for each table, make a column array, "columnArray" and save to dummyDataObject (Options: 1. run sql query to get column names) +// 8. generate table data and save as tableMatrix (transpose of table) to dummyDataObject (importing in dataObj from state, created in dummyData process) +// iterate over "column array" +// declare capture array +// generate n entries for column where n is equal to the number of rows asked for in dataObj (using faker) +// push these entries to capture array +// push capture array to matrix + +// note: this ^ only works for single-column primary and foreign key constraints (no composite keys) +// 9. use helper function to write insert statements to a string (importing in dataObj from state, created in dummyData process), e.g.: +// function generateInsertQueries (tableName, tableMatrix, columnArray) { +// let dumpString = ''; +// let catchValuesForQuery = []; +// let query = ''; +// iterate from j = 0 while j < tableMatrix[0].length +// catchValuesForQuery = []; +// iterate from i = 0 while i < tableMatrix.length (= columnArray.length) +// push tableMatrix[i][j] entry into catchValuesForQuery array +// query = "INSERT INTO {tableName} ({...columnArray}) VALUES ({...catchValuesForQuery});" +// dumpString += query; +// return query; +// } +// 10. write query string to the .sql file + +//parameter passed in from channels +type schemaLayout = { + tableNames: string[]; + tables: any; +} + +//parameter passed in from channels +type dummyDataRequest = { + schemaName: string; + dummyData: {}; +} + +//created in function +// type dummyDataObject = {}; + +//this function generates unique values for a column +const generatePrimayKey = () => { + +} + +// this function generates non-unique data for a column +// dataType should be an object +// 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': + 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') + } +}; + +//helper function to generate random numbers that will ultimately represent a random date +function 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 +} + +module.exports = { + + writeCSVFile: (tableMatrix, tableName, columnArray) => { + 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 = []; + } + //join tableMatrix with a line break + const tableDataString: string = table.join('\n'); + + const columnString: string = columnArray.join(','); + + const csvString: string = columnString.concat('\n').concat(tableDataString); + // build file path + const compiledPath = path.join(__dirname, `../${tableName}.csv`); + //write csv file + return new Promise((resolve, reject) => { + fs.writeFile(compiledPath, csvString, (err: any) => { + if (err) throw err; + resolve(console.log('FILE SAVED')); + // reject(console.log('Error Saving File')) + }); + }) + }, + + //maps table names from schemaLayout to sql files + generateDummyData: (schemaLayout, dummyDataRequest) => { + 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 = []; + //iterate over columnArray (schemaLayout.tableLayout[tableName]) + for (let i = 0; i < schemaLayout.tables[tableName].length; i++) { + //while i < reqeusted number of tables + while (columnData.length < dummyDataRequest.dummyData[tableName]) { + //generate an entry + let entry = generateDataByType(schemaLayout.tables[tableName][i]); + //push into columnData + columnData.push(entry); + }; + //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; + } +} + +// export default generateDummyDataQueries + + // //iterate through tables for which data was requested to copy data to tables + // for(let i = 0; i < arrayOfTableMatrices.length; i++) { + // // pulling tableName out of schemaLayout + // let tableName = schemaLayout.tableNames[i]; + // // if data was requested for that table, then copy the csv file data to the table + // if (dummyDataRequest.dummyData[tableName]){ + // await copyCSVToTable (tableName); + // } + // } + +// //this function copies data to a table from an epynomous .csv file +// const copyCSVToTable = (tableName) => { +// const compiledPath = path.join(__dirname, `../${tableName}.csv`); +// execute(`docker exec postgres-1 COPY ${tableName} FROM ${compiledPath} DELIMITER ',' CSV HEADER;`, null); +// } \ No newline at end of file diff --git a/backend/newDummyD/foreign_key_info.ts b/backend/newDummyD/foreign_key_info.ts new file mode 100644 index 00000000..20c6ac75 --- /dev/null +++ b/backend/newDummyD/foreign_key_info.ts @@ -0,0 +1,14 @@ +const getForeignKeyQueryString: string = ` + SELECT + tc.table_name AS primary_table_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY';` diff --git a/backend/newDummyD/note.txt b/backend/newDummyD/note.txt new file mode 100644 index 00000000..bc46c294 --- /dev/null +++ b/backend/newDummyD/note.txt @@ -0,0 +1,111 @@ +1. get foreign key join table + SELECT + tc.table_name AS primary_table_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM + information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY'; +2. create a foreign key object, e.g.: + const FK = { + // people: + foreignTable_1: { + // species: 10,000 (sent from the dummyData modal) + primaryTable: numOfRows, + // foreignKeyColumns: + foreignKeyColumns: { + // species_id: true + foreignKeyColumnName_1: true, + foreignKeyColumnName_2: true + } + } + } +3. get schema layout + const schemaLayout: any = { + // tableNames: ['Johnny Bravo', 'Teen Titans', ...] + tableNames: [], + tables: { + // table name: [columnNames array] + } + }; +4. iterate over tables in hollow schema (Options: 1. run sql query to get list of tables; 2. use 'getList' function defined in models.ts) + 5. (Was moved to inside step 9) + 6. create dummyData object, e.g.: + const DD = { + // people + table_1: { + // columnArray: ["_id", "name", "homeworld_id", ...] + columnArray: [], + // [[1,2,3,4,...],["luke", "leia", "jabaDaHut", ...], ...] + tableMatrix: [[],[]] + } + } + 7. for each table, make a column array, "columnArray" and save to dummyDataObject (Options: 1. run sql query to get column names) + 8. generate table data and save as tableMatrix (transpose of table) to dummyDataObject (importing in dataObj from state, created in dummyData process) + iterate over "column array" + declare capture array + generate n entries for column where n is equal to the number of rows asked for in dataObj (using faker) + push these entries to capture array + push capture array to matrix + + note: this ^ only works for single-column primary and foreign key constraints (no composite keys) + 9. use helper function to write insert statements to a string (importing in dataObj from state, created in dummyData process), e.g.: + function generateInsertQueries(tableName, tableMatrix, columnArray) { + //transpose tableMatrix + + //join each subarray (which correspond to rows in our table) with a comma + + //join tableMatrix with a line break + + //save to string + + //generate .csv file + + //write string to file + + //run postgres COPY table_name FROM 'filepath/path/postgres-data.csv' (absolute path) DELIMTERE ',' CSV HEADER; + + //delete local csv file + + //tableMatrix: [[1,2,3,],[]] + name // [[1,2,3,4,...],["luke", "leia", "jabaDaHut", ...], ...] + //tableName: people + //tableMatrix: [[1,2,3,4,...],["luke", "leia", "jabaDaHut", ...], ...] + //columnArray: ["_id", "name", "homeworld_id", ...] + + + //want a string separated by commas, ends with /n, concatenated with other rows + 5. generate new .csv file + } + + + + 10. write query string to the .sql file +11. (after finished looping over tables in hollow schema) iterate over table names to run postgres command to write inserts from created sql files + "psql -U -d -f .sql" +12. delete files afterword + + + +9 (previous). + +function generateInsertQueries (tableName, tableMatrix, columnArray) { + +let dumpString = ''; + let catchValuesForQuery = []; + let query = ''; + iterate from j = 0 while j < tableMatrix[0].length + catchValuesForQuery = []; + iterate from i = 0 while i < tableMatrix.length (= columnArray.length) + push tableMatrix[i][j] entry into catchValuesForQuery array + query = "INSERT INTO {tableName} ({...columnArray}) VALUES ({...catchValuesForQuery});" + dumpString += query; + return query; +} + diff --git a/frontend/assets/stylesheets/css/components.css b/frontend/assets/stylesheets/css/components.css index d630b813..dfda5e0f 100644 --- a/frontend/assets/stylesheets/css/components.css +++ b/frontend/assets/stylesheets/css/components.css @@ -89,7 +89,7 @@ input { background-color: #444c50; border: none; padding: 7px; - min-width: 240px; + min-width: 20em; outline: none; } @@ -97,6 +97,103 @@ 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 { + 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: center; + -ms-flex-pack: center; + justify-content: center; +} + +.dummy-data-table { + 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; @@ -129,7 +226,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 +236,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..19607402 100644 --- a/frontend/assets/stylesheets/css/layout.css +++ b/frontend/assets/stylesheets/css/layout.css @@ -1,25 +1,46 @@ @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: flex; - flex: 1; - flex-direction: column; - justify-content: center; - align-items: center; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -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; } @@ -53,8 +74,13 @@ } #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 +90,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: flex; - flex-direction: column; - flex-grow: 1; + 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-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..fa4ece85 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,100 @@ } .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; } diff --git a/frontend/assets/stylesheets/css/style.css b/frontend/assets/stylesheets/css/style.css index c2f2c89b..51194637 100644 --- a/frontend/assets/stylesheets/css/style.css +++ b/frontend/assets/stylesheets/css/style.css @@ -95,7 +95,7 @@ input { background-color: #444c50; border: none; padding: 7px; - min-width: 240px; + min-width: 20em; outline: none; } @@ -103,6 +103,103 @@ 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 { + 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: center; + -ms-flex-pack: center; + justify-content: center; +} + +.dummy-data-table { + 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; @@ -135,7 +232,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,26 +243,54 @@ 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; } @@ -198,8 +324,13 @@ input *:focus { } #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 +340,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 +517,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 +548,102 @@ 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; + -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; - flex-direction: row; + -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; } -.modal-buttons .input-button { - margin-left: 15px; +#copy-button { + margin-top: 20px; } * { @@ -387,13 +682,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 +725,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 +742,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 +767,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..c7bc4440 100644 --- a/frontend/assets/stylesheets/scss/components.scss +++ b/frontend/assets/stylesheets/scss/components.scss @@ -94,13 +94,78 @@ 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; +} + +.dummy-data-table { + 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; @@ -145,3 +210,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..cadad057 100644 --- a/frontend/assets/stylesheets/scss/layout.scss +++ b/frontend/assets/stylesheets/scss/layout.scss @@ -59,7 +59,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..2ab5c280 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,69 @@ 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; } + + + diff --git a/frontend/components/App.tsx b/frontend/components/App.tsx index 873d26d2..39fdc60d 100644 --- a/frontend/components/App.tsx +++ b/frontend/components/App.tsx @@ -34,9 +34,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 }); } diff --git a/frontend/components/MainPanel.tsx b/frontend/components/MainPanel.tsx index b9fe3e2a..dfbca5e1 100644 --- a/frontend/components/MainPanel.tsx +++ b/frontend/components/MainPanel.tsx @@ -58,26 +58,31 @@ 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 + } + })) }) } 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() { + console.log('Main Panel: ', this.state.lists); return (
- +
); } diff --git a/frontend/components/Splash.tsx b/frontend/components/Splash.tsx index 3ac33313..da18833b 100644 --- a/frontend/components/Splash.tsx +++ b/frontend/components/Splash.tsx @@ -21,8 +21,8 @@ export class Splash extends Component {

Import database in .sql or .tar?

- - + +
); diff --git a/frontend/components/leftPanel/Compare.tsx b/frontend/components/leftPanel/Compare.tsx index 0965920e..8294a466 100644 --- a/frontend/components/leftPanel/Compare.tsx +++ b/frontend/components/leftPanel/Compare.tsx @@ -17,31 +17,50 @@ 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}); }; 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 +97,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 +196,7 @@ export const Compare = (props: CompareProps) => {
{ fontSize: 16 }, legend: { - display: false, + display: true, position: 'right' - } + }, + maintainAspectRatio: false }} />
diff --git a/frontend/components/rightPanel/SchemaContainer.tsx b/frontend/components/rightPanel/SchemaContainer.tsx index 4864676c..30be9eae 100644 --- a/frontend/components/rightPanel/SchemaContainer.tsx +++ b/frontend/components/rightPanel/SchemaContainer.tsx @@ -6,6 +6,7 @@ import Query from './schemaChildren/Query'; type SchemaContainerProps = { queries: any; currentSchema: string; + tableList: string[]; }; type state = { @@ -26,12 +27,16 @@ export class SchemaContainer extends Component {
- - -
-
- +
+ +
+
+ +
+ {/*
+ +
*/}
); diff --git a/frontend/components/rightPanel/Tabs.tsx b/frontend/components/rightPanel/Tabs.tsx index f7e66d47..0efad9b5 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) => { @@ -79,11 +80,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..9d175f24 100644 --- a/frontend/components/rightPanel/schemaChildren/Data.tsx +++ b/frontend/components/rightPanel/schemaChildren/Data.tsx @@ -21,10 +21,6 @@ export class Data extends Component { return (
-
-
-
-

Data Table

{queries.length === 0 ? null :
} diff --git a/frontend/components/rightPanel/schemaChildren/DummyDataModal.tsx b/frontend/components/rightPanel/schemaChildren/DummyDataModal.tsx new file mode 100644 index 00000000..b2438fa6 --- /dev/null +++ b/frontend/components/rightPanel/schemaChildren/DummyDataModal.tsx @@ -0,0 +1,195 @@ +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 DummyDataModalProps = { + show: boolean; + showModal: any; + onClose: any; + currentSchema: string; + tableList: string[]; +}; + +type state = { + currentTable: string, + dataInfo: {}, + rowNumber: string +} + +class DummyDataModal extends Component { + + constructor(props: DummyDataModalProps) { + 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) => { + this.setState({currentTable: eventKey}); + }; + + //function to generate the dropdown optiosn from the table names in state + dropDownList = () => { + const result: any = []; + let tableName; + 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}); + } + } + 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) => { + //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 modal's table + this.setState({dataInfo: {}}); + } + + render() { + if (this.props.show === false) { + return null; + } + + 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 DummyDataModal; \ No newline at end of file diff --git a/frontend/components/rightPanel/schemaChildren/Query.tsx b/frontend/components/rightPanel/schemaChildren/Query.tsx index 67d9532a..6269e6c1 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 DummyDataModal from './DummyDataModal'; 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,6 +33,7 @@ class Query extends Component { super(props); this.handleQuerySubmit = this.handleQuerySubmit.bind(this); this.updateCode = this.updateCode.bind(this); + this.handleTrackQuery = this.handleTrackQuery.bind(this); // this.handleQueryPrevious = this.handleQueryPrevious.bind(this); // this.handleGenerateData = this.handleGenerateData.bind(this); } @@ -34,6 +42,7 @@ class Query extends Component { queryString: '', queryLabel: '', show: false, + trackQuery: false }; // Updates state.queryString as user inputs query label @@ -41,6 +50,11 @@ class Query extends Component { 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,17 +65,34 @@ 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: '' }); } } @@ -79,31 +110,46 @@ class Query extends Component { return (
+
+ +

Query

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

- + {/* */}


-

*required

- {/* */}
); } 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 (