diff --git a/.gitignore b/.gitignore index b3bf3411..37c9d999 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ dvdrental.tar *.sql debug.log coverage/ -release-builds/ \ No newline at end of file +release-builds/ +UserTableLayouts.json \ No newline at end of file diff --git a/README.md b/README.md index b96cadad..c0412f62 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,15 @@ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/oslabs-beta/SeeQR) -![Release: 6.0.1](https://img.shields.io/badge/Release-6.0.1-red) +![Release: 7.0.1](https://img.shields.io/badge/Release-7.0.0-red) ![License: MIT](https://img.shields.io/badge/License-MIT-orange.svg) ![Contributions Welcome](https://img.shields.io/badge/Contributions-welcome-blue.svg) [![Twitter](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Ftheseeqr)](https://twitter.com/theseeqr) [![Github stars](https://img.shields.io/github/stars/open-source-labs/SeeQR?style=social)](https://github.com/open-source-labs/SeeQR) -[![Tests](https://github.com/open-source-labs/SeeQR/actions/workflows/test.yml/badge.svg)](https://github.com/open-source-labs/SeeQR/actions/workflows/test.yml) [SeeQR.info](http://www.seeqr.info) -

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

+

SeeQR is a convenient one-stop shop for efficient SQL database manipulation and performance testing. SeeQR can be used throughout the database life-cycle, from creation to testing.

@@ -33,6 +32,15 @@ To get started on contributing to this project: 1. Download and install [Postgres.app](https://postgresapp.com/)(Mac)/[PGAdmin](https://www.pgadmin.org/download)(Windows) and start it before opening up SeeQR 2. Ensure that psql is available in the `$PATH` 3. Ensure that a 'postgres' role exists + - Open a database in Postgres + - Use command: + - `SET ROLE postgres` +4. Ensure that your Postgres instance is running on port 5432 + - If there is currently an instance running on port 5432 + - Open a new terminal + - Use command to end all: + - `sudo pkill -u postgres` + - Start Postgres instance on port 5432 4. Download the latest version of [SeeQR](https://github.com/open-source-labs/seeqr/releases/latest) ## Built With @@ -60,23 +68,24 @@ To get started on contributing to this project: - Copying an existing database (with or without original data) - Users can export any database onto their local machine - Users can toggle between the 'DATABASES' view and the 'QUERIES' view + - Users can toggle between an 'ER DIAGRAM' view and the 'TABLES' view for each database
- Databases - - - In the 'DATABASES' view, users can select a table from a list of all the tables in the schema of the currently selected database - - Information about the selected table is then displayed - - The name and size of the selected database are also displayed at the top of the page - - Users can also generate large amounts of foreign-key compliant dummy data for the selected table in the current database. Currently supported data types are: - - INT - - SMALLINT - - BIGINT - - VARCHAR - - BOOLEAN - - DATE + - In the 'DATABASES' view, an interactive Entity Relationship Diagram (`ER DIAGRAM`) is displayed for the selected database + - Users can select `TABLE` to see selected database in tabular form + - Users can select a table from a list of all the tables in the schema of the currently selected database + - Information about the selected table is then displayed + - The name and size of the selected database are also displayed at the top of the page + - Users can also generate large amounts of foreign-key compliant dummy data for the selected table in the current database. Currently supported data types are: + - INT + - BIGINT + - VARCHAR + - BOOLEAN + - DATE
@@ -86,14 +95,15 @@ To get started on contributing to this project: - Users can create a new database from scratch by clicking the `Create New Database` button at the bottom of the sidebar - Once a the database is given a name, hitting the `Initialize Database` button will create new database on the users PostgreSQL instance - - Users can then input SQL commands and click `Update Database` to create and drop tables in the database - - Users have the option to alter any existing databases as well by selecting the database on the sidebar and running any SQL commands they would like. + - Users can modify the newly created database as well as any existing databases using the `ER Diagram` to create/change/delete tables and columns + - Users also have the option to alter any newly created / existing databases by inputting SQL commands and click `Update Database` - The `Export` button will write a .sql file on the user's desktop of the selected database
+
- Queries @@ -164,7 +174,7 @@ We've released SeeQR because it's a useful tool to help optimize SQL databases. ## Core Team -[Allison Le](https://github.com/allisonle1) | [Brandon Lee](https://github.com/BrandonW-Lee) | [Casey Escovedo](https://github.com/caseyescovedo) | [Casey Walker](https://github.com/cwalker3011) | [Catherine Chiu](https://github.com/catherinechiu) | [Chris Akinrinade](https://github.com/chrisakinrinade) | [Cindy Chau](https://github.com/cindychau) | [Claudio Santos](https://github.com/Claudiohbsantos) | [Eric Han](https://github.com/ericJH92) | [Faraz Akhtar](https://github.com/faraza22) | [Frank Norton](https://github.com/FrankNorton32) | [Harrison Nam](https://github.com/harrynam07) | [James Kolotouros](https://github.com/dkolotouros) | [Jennifer Courtner](https://github.com/jcourtner) | [John Wagner](https://github.com/jwagner988) | [Justin Dury-Agri](https://github.com/justinD-A) | [Justin Hicks](https://github.com/JuiceBawks) | [Katie Klochan](https://github.com/kklochan) | [May Wirapa Boonyasurat](https://github.com/mimiwrp) | [Mercer Stronck](https://github.com/mercerstronck) | [Muhammad Trad](https://github.com/muhammadtrad) | [Richard Guo](https://github.com/richardguoo) | [Richard Lam](https://github.com/rlam108) | [Sam Frakes](https://github.com/frakes413) | [Serena Kuo](https://github.com/serenackuo) | [Timothy Sin](https://github.com/timothysin) | [Vincent Trang](https://github.com/vincentt114) +[Michelle Chang](https://github.com/mkchang168) | [Jake Bradbeer](https://github.com/JBradbeer) | [Bryan Santos](https://github.com/santosb93) | [William Trey Lewis](https://github.com/treyfrog128) | [Brandon Lee](https://github.com/BrandonW-Lee) | [Casey Escovedo](https://github.com/caseyescovedo) | [Casey Walker](https://github.com/cwalker3011) | [Catherine Chiu](https://github.com/catherinechiu) | [Chris Akinrinade](https://github.com/chrisakinrinade) | [Cindy Chau](https://github.com/cindychau) | [Claudio Santos](https://github.com/Claudiohbsantos) | [Eric Han](https://github.com/ericJH92) | [Faraz Akhtar](https://github.com/faraza22) | [Frank Norton](https://github.com/FrankNorton32) | [Harrison Nam](https://github.com/harrynam07) | [James Kolotouros](https://github.com/dkolotouros) | [Jennifer Courtner](https://github.com/jcourtner) | [John Wagner](https://github.com/jwagner988) | [Justin Dury-Agri](https://github.com/justinD-A) | [Justin Hicks](https://github.com/JuiceBawks) | [Katie Klochan](https://github.com/kklochan) | [May Wirapa Boonyasurat](https://github.com/mimiwrp) | [Mercer Stronck](https://github.com/mercerstronck) | [Muhammad Trad](https://github.com/muhammadtrad) | [Richard Guo](https://github.com/richardguoo) | [Richard Lam](https://github.com/rlam108) | [Sam Frakes](https://github.com/frakes413) | [Serena Kuo](https://github.com/serenackuo) | [Timothy Sin](https://github.com/timothysin) | [Vincent Trang](https://github.com/vincentt114) ## License diff --git a/assets/readmeImages/gifs/Query_Execution.gif b/assets/readmeImages/gifs/Query_Execution.gif index e4e8873f..23d6dbc9 100644 Binary files a/assets/readmeImages/gifs/Query_Execution.gif and b/assets/readmeImages/gifs/Query_Execution.gif differ diff --git a/assets/readmeImages/gifs/create_db.gif b/assets/readmeImages/gifs/create_db.gif index abbf3f99..fd5615ef 100644 Binary files a/assets/readmeImages/gifs/create_db.gif and b/assets/readmeImages/gifs/create_db.gif differ diff --git a/assets/readmeImages/gifs/dummy_data.gif b/assets/readmeImages/gifs/dummy_data.gif index ed7b5254..a20c2be3 100644 Binary files a/assets/readmeImages/gifs/dummy_data.gif and b/assets/readmeImages/gifs/dummy_data.gif differ diff --git a/assets/readmeImages/gifs/modify_db.gif b/assets/readmeImages/gifs/modify_db.gif new file mode 100644 index 00000000..dcf3558a Binary files /dev/null and b/assets/readmeImages/gifs/modify_db.gif differ diff --git a/assets/readmeImages/gifs/quick_start.gif b/assets/readmeImages/gifs/quick_start.gif index 3034a0ba..d39d3dad 100644 Binary files a/assets/readmeImages/gifs/quick_start.gif and b/assets/readmeImages/gifs/quick_start.gif differ diff --git a/backend/BE_types.ts b/backend/BE_types.ts index c9d9e322..c3114598 100644 --- a/backend/BE_types.ts +++ b/backend/BE_types.ts @@ -1,6 +1,9 @@ /** * This file contains common types that need to be used across the backend */ + import { + UpdatesObjType +} from '../frontend/types'; export interface ColumnObj { column_name: string; @@ -28,3 +31,8 @@ export interface DBList { } export type DummyRecords = [string[], ...Array<(string | number)[]>]; + +export type BackendObjType = { + database: string; + updates: UpdatesObjType; +}; diff --git a/backend/DummyD/dummyDataMain.ts b/backend/DummyD/dummyDataMain.ts index 6e3bd95c..1743c0ee 100644 --- a/backend/DummyD/dummyDataMain.ts +++ b/backend/DummyD/dummyDataMain.ts @@ -71,7 +71,8 @@ type GenerateDummyData = (tableInfo: ColumnObj[], numRows: number) => Promise { // assuming primary key is serial, get all the column names except for the column with the primary key const columnNames = tableInfo.reduce((acc: string[], curr: ColumnObj) => { - if (curr.constraint_type !== 'PRIMARY KEY') acc.push(curr.column_name); + // if (curr.constraint_type !== 'PRIMARY KEY') + acc.push(curr.column_name); return acc; }, []); const dummyRecords: DummyRecords = [columnNames]; @@ -83,8 +84,8 @@ const generateDummyData: GenerateDummyData = async (tableInfo: ColumnObj[], numR for (let j = 0; j < tableInfo.length; j += 1) { // if column has no foreign key constraint, then generate dummy data based on data type if ( - tableInfo[j].constraint_type !== 'FOREIGN KEY' && - tableInfo[j].constraint_type !== 'PRIMARY KEY' + tableInfo[j].constraint_type !== 'FOREIGN KEY' + // && tableInfo[j].constraint_type !== 'PRIMARY KEY' ) row.push(generateDataByType(tableInfo[j])); // if there is a foreign key constraint, grab random key from foreign table @@ -94,11 +95,15 @@ const generateDummyData: GenerateDummyData = async (tableInfo: ColumnObj[], numR const foreignTable = tableInfo[j].foreign_table; const getForeignKeyQuery = ` SELECT ${foreignColumn} - FROM ${foreignTable} TABLESAMPLE BERNOULLI(50) + FROM ${foreignTable} TABLESAMPLE BERNOULLI(100) LIMIT 1 `; const foreignKey = await db.query(getForeignKeyQuery); - if (foreignKey.rows.length) row.push(foreignKey.rows[0]['_id']); + const chosenPrimaryValue = foreignKey.rows[0][Object.keys(foreignKey.rows[0])[0]] + if (foreignKey.rows.length) { + if (typeof chosenPrimaryValue === 'string') row.push(`'${chosenPrimaryValue}'`); + else row.push(chosenPrimaryValue); + } else return new Error('There was an error while retrieving a valid foreign key.'); } catch(err) { return err; diff --git a/backend/channels.ts b/backend/channels.ts index 39a06a44..c8505200 100644 --- a/backend/channels.ts +++ b/backend/channels.ts @@ -5,6 +5,7 @@ import os from 'os'; import helperFunctions from './helperFunctions'; import generateDummyData from './DummyD/dummyDataMain'; import { ColumnObj, DBList, DummyRecords } from './BE_types'; +import backendObjToQuery from './ertable-functions'; const db = require('./models'); @@ -59,23 +60,26 @@ ipcMain.handle( ); // Deletes the DB that is passed from the front end and returns an updated DB List -ipcMain.handle('drop-db', async (event, dbName: string, currDB: boolean): Promise => { - event.sender.send('async-started'); - try { - // if deleting currently connected db, disconnect from db - if (currDB) await db.connectToDB(''); +ipcMain.handle( + 'drop-db', + async (event, dbName: string, currDB: boolean): Promise => { + event.sender.send('async-started'); + try { + // if deleting currently connected db, disconnect from db + if (currDB) await db.connectToDB(''); - // drop db - const dropDBScript = dropDBFunc(dbName); - await db.query(dropDBScript); + // drop db + const dropDBScript = dropDBFunc(dbName); + await db.query(dropDBScript); - // send updated db info - const dbsAndTables: DBList = await db.getLists(); - event.sender.send('db-lists', dbsAndTables); - } finally { - event.sender.send('async-complete'); + // send updated db info + const dbsAndTables: DBList = await db.getLists(); + event.sender.send('db-lists', dbsAndTables); + } finally { + event.sender.send('async-complete'); + } } -}); +); interface DuplicatePayload { newName: string; @@ -134,7 +138,7 @@ ipcMain.handle( const dbsAndTableInfo: DBList = await db.getLists(); event.sender.send('db-lists', dbsAndTableInfo); } finally { - // //cleanup temp file + // //cleanup temp file try { fs.unlinkSync(tempFilePath); } catch (e) { @@ -201,7 +205,7 @@ interface QueryPayload { selectedDb: string; } -// Run query passed from the front-end, and send back an updated DB List +// Run query passed from the front-end, and send back an updated DB List // DB will rollback if query is unsuccessful ipcMain.handle( 'run-query', @@ -251,48 +255,38 @@ ipcMain.handle( } ); -interface ExportPayload { +interface ExportPayload { sourceDb: string; } -ipcMain.handle( - 'export-db', - async (event, { sourceDb }: ExportPayload) => { - event.sender.send('async-started'); - // store temporary file in user desktop - const FilePath = path.resolve( - os.homedir(), - 'desktop', - `${sourceDb}.sql` - ); +ipcMain.handle('export-db', async (event, { sourceDb }: ExportPayload) => { + event.sender.send('async-started'); + // store temporary file in user desktop + const FilePath = path.resolve(os.homedir(), 'desktop', `${sourceDb}.sql`); let feedback: Feedback = { - type: '', - message: '', - }; + type: '', + message: '', + }; try { // dump database to new file const dumpCmd = runFullCopyFunc(sourceDb, FilePath); - + try { await promExecute(dumpCmd); feedback = { type: 'success', - message: `${sourceDb} Schema successfully exported to ${FilePath}` - } + message: `${sourceDb} Schema successfully exported to ${FilePath}`, + }; event.sender.send('feedback', feedback); } catch (e) { - throw new Error( - `Failed to dump ${sourceDb} to a file at ${FilePath}` - ); - } - } - finally { - event.sender.send('async-complete'); - } + throw new Error(`Failed to dump ${sourceDb} to a file at ${FilePath}`); } -); + } finally { + event.sender.send('async-complete'); + } +}); interface dummyDataRequestPayload { dbName: string; @@ -314,8 +308,10 @@ ipcMain.handle( const tableInfo: ColumnObj[] = await db.getTableInfo(data.tableName); // generate dummy data - const dummyArray: DummyRecords = await generateDummyData(tableInfo, data.rows); - + const dummyArray: DummyRecords = await generateDummyData( + tableInfo, + data.rows + ); // generate insert query string to insert dummy records const columnsStringified = '(' .concat(dummyArray[0].join(', ')) @@ -331,7 +327,6 @@ ipcMain.handle( .concat(dummyArray[dummyArray.length - 1].join(', ')) .concat(');'); insertQuery = insertQuery.concat(lastRecordStringified); - // insert dummy records into DB await db.query('Begin;'); await db.query(insertQuery); @@ -354,7 +349,7 @@ ipcMain.handle( // send feedback back to FE event.sender.send('feedback', feedback); - + // send notice to FE that DD generation has been completed event.sender.send('async-complete'); } @@ -380,13 +375,11 @@ ipcMain.handle( // update DBList in the sidebar to show this new db const dbsAndTableInfo: DBList = await db.getLists(); event.sender.send('db-lists', dbsAndTableInfo); - } catch (e) { // in the case of an error, delete the created db const dropDBScript = dropDBFunc(newDbName); await db.query(dropDBScript); throw new Error('Failed to initialize new database'); - } finally { event.sender.send('async-complete'); } @@ -400,7 +393,7 @@ interface UpdatePayload { selectedDb: string; } -// Run query passed from the front-end, and send back an updated DB List +// Run query passed from the front-end, and send back an updated DB List // DB will rollback if query is unsuccessful ipcMain.handle( 'update-db', @@ -414,15 +407,11 @@ ipcMain.handle( // Run Query // let returnedRows; try { - await db.query(sqlString); - } catch (e) { if (e) throw new Error('Failed to update schema'); } - } finally { - // send updated db info in case query affected table or database information // must be run after we connect back to the originally selected so tables information is accurate const dbsAndTables: DBList = await db.getLists(); @@ -431,4 +420,46 @@ ipcMain.handle( event.sender.send('async-complete'); } } -); \ No newline at end of file +); + +// Generate and run query from react-flow ER diagram +ipcMain.handle('ertable-schemaupdate', async (event, backendObj) => { + // send notice to front end that schema update has started + event.sender.send('async-started'); + let feedback: Feedback = { + type: '', + message: '', + }; + try { + // Generates query from backendObj + const query = backendObjToQuery(backendObj); + // run sql command + await db.query('Begin;'); + await db.query(query); + await db.query('Commit;'); + feedback = { + type: 'success', + message: 'Database updated successfully.', + }; + return 'success'; + } catch (err) { + // rollback transaction if there's an error in update and send back feedback to FE + await db.query('Rollback;'); + + feedback = { + type: 'error', + message: err, + }; + } finally { + // send updated db info + const updatedDb: DBList = await db.getLists(); + event.sender.send('db-lists', updatedDb); + + // send feedback back to FE + event.sender.send('feedback', feedback); + + // send notice to FE that schema update has been completed + event.sender.send('async-complete'); + + } +}); diff --git a/backend/ertable-functions.ts b/backend/ertable-functions.ts new file mode 100644 index 00000000..7e2f1b9e --- /dev/null +++ b/backend/ertable-functions.ts @@ -0,0 +1,233 @@ +import { + AddTablesObjType, + DropTablesObjType, + AlterTablesObjType, + AlterColumnsObjType, + AddConstraintObjType, +} from '../frontend/types'; + +import { + BackendObjType, +} from './BE_types'; + +function backendObjToQuery(backendObj: BackendObjType): string { + const outputArray: string[] = []; + + // Add table to database + function addTable(addTableArray: AddTablesObjType[]): void { + for (let i = 0; i < addTableArray.length; i += 1) { + const currTable: AddTablesObjType = addTableArray[i]; + outputArray.push( + `CREATE TABLE ${currTable.table_schema}.${currTable.table_name}(); ` + ); + } + } + // Remove table from database + function dropTable(dropTableArray: DropTablesObjType[]): void { + for (let i = 0; i < dropTableArray.length; i += 1) { + const currTable: DropTablesObjType = dropTableArray[i]; + outputArray.push( + `DROP TABLE ${currTable.table_schema}.${currTable.table_name}; ` + ); + } + } + // Alter existing table in database. All column functions reside under this function + function alterTable(alterTableArray: AlterTablesObjType[]): void { + // Add column to table + function addColumn(currTable: AlterTablesObjType): string { + let addColumnString: string = ''; + if (currTable.addColumns.length) { + for (let i = 0; i < currTable.addColumns.length; i += 1) { + addColumnString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ADD COLUMN ${currTable.addColumns[i].column_name} ${currTable.addColumns[i].data_type}(${currTable.addColumns[i].character_maximum_length}); `; + } + } + return addColumnString; + } + // Remove column from table + function dropColumn(currTable: AlterTablesObjType): string { + let dropColumnString: string = ''; + if (currTable.dropColumns.length) { + for (let i = 0; i < currTable.dropColumns.length; i += 1) { + dropColumnString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} DROP COLUMN ${currTable.dropColumns[i].column_name}; `; + } + } + return dropColumnString; + } + // Add/remove constraints from column + function alterTableConstraint(currTable: AlterTablesObjType): string { + let alterTableConstraintString: string = ''; + // Add a primary key constraint to column + function addPrimaryKey(currConstraint: AddConstraintObjType, currColumn: AlterColumnsObjType): void { + let defaultRowValue: number | string; + if(currColumn.current_data_type === 'character varying') defaultRowValue = 'A'; + else defaultRowValue = 1; + alterTableConstraintString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ADD CONSTRAINT ${currConstraint.constraint_name} PRIMARY KEY (${currColumn.column_name}); INSERT INTO ${currTable.table_schema}.${currTable.table_name} (${currColumn.column_name}) VALUES ('${defaultRowValue}'); `; + } + // Add a foreign key constraint to column + function addForeignKey(currConstraint: AddConstraintObjType, currColumn: AlterColumnsObjType): void { + alterTableConstraintString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ADD CONSTRAINT ${currConstraint.constraint_name} FOREIGN KEY ("${currColumn.column_name}") REFERENCES ${currConstraint.foreign_table}(${currConstraint.foreign_column}); `; + } + // Add a unique constraint to column + function addUnique(currConstraint: AddConstraintObjType, currColumn: AlterColumnsObjType): void { + alterTableConstraintString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ADD CONSTRAINT ${currConstraint.constraint_name} UNIQUE (${currColumn.column_name}); `; + } + // Remove constraint from column + function dropConstraint(currDrop): void { + alterTableConstraintString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} DROP CONSTRAINT ${currDrop}; `; + } + + for (let i = 0; i < currTable.alterColumns.length; i += 1) { + const currColumn: AlterColumnsObjType = currTable.alterColumns[i]; + for (let j = 0; j < currColumn.add_constraint.length; j += 1) { + const currConstraint: AddConstraintObjType = currColumn.add_constraint[j]; + if (currConstraint.constraint_type === 'PRIMARY KEY') { + addPrimaryKey(currConstraint, currColumn); + } else if (currConstraint.constraint_type === 'FOREIGN KEY') { + addForeignKey(currConstraint, currColumn); + } else if (currConstraint.constraint_type === 'UNIQUE') { + addUnique(currConstraint, currColumn); + } + } + for (let j = 0; j < currColumn.drop_constraint.length; j += 1) { + const currDrop: string = currColumn.drop_constraint[j]; + dropConstraint(currDrop); + } + } + return alterTableConstraintString; + } + // Add/remove not null constraint from column + function alterNotNullConstraint(currTable: AlterTablesObjType): string { + let notNullConstraintString: string = ''; + for (let i = 0; i < currTable.alterColumns.length; i += 1) { + if (currTable.alterColumns[i].is_nullable === 'NO') { + notNullConstraintString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ALTER COLUMN ${currTable.alterColumns[i].column_name} SET NOT NULL; `; + } + if (currTable.alterColumns[i].is_nullable === 'YES') { + notNullConstraintString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ALTER COLUMN ${currTable.alterColumns[i].column_name} DROP NOT NULL; `; + } + } + return notNullConstraintString; + } + // Change the data type of the column + function alterType(currTable: AlterTablesObjType): string { + let alterTypeString: string = ''; + for (let i = 0; i < currTable.alterColumns.length; i += 1) { + if (currTable.alterColumns[i].data_type !== null) { + if (currTable.alterColumns[i].data_type === 'date') { + alterTypeString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ALTER COLUMN ${currTable.alterColumns[i].column_name} TYPE date USING ${currTable.alterColumns[i].column_name}::text::date; `; + } else { + alterTypeString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ALTER COLUMN ${currTable.alterColumns[i].column_name} TYPE ${currTable.alterColumns[i].data_type} USING ${currTable.alterColumns[i].column_name}::${currTable.alterColumns[i].data_type}; `; + } + } + } + return alterTypeString; + } + // Change the max character length of a varchar + function alterMaxCharacterLength(currTable: AlterTablesObjType): string { + let alterMaxCharacterLengthString: string = ''; + for (let i = 0; i < currTable.alterColumns.length; i += 1) { + if (currTable.alterColumns[i].character_maximum_length) { + alterMaxCharacterLengthString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ALTER COLUMN ${currTable.alterColumns[i].column_name} TYPE varchar(${currTable.alterColumns[i].character_maximum_length}); `; + } + } + return alterMaxCharacterLengthString; + } + + for (let i = 0; i < alterTableArray.length; i += 1) { + const currTable: AlterTablesObjType = alterTableArray[i]; + outputArray.push( + `${addColumn(currTable)}${dropColumn(currTable)}${alterType(currTable)}${alterTableConstraint( + currTable + )}${alterNotNullConstraint(currTable)}${alterMaxCharacterLength(currTable)}` + ); + } + } + // Outer function to rename tables and columns. Will rename columns first, then rename tables + function renameTablesColumns(renameTableArray: AlterTablesObjType[]): void { + let renameString: string = ''; + const columnsNames: object = {}; + const tablesNames: object = {}; + const constraintsNames: object = {}; + // Populates the tablesNames object with new table names + function renameTable(currTable: AlterTablesObjType): void { + if (currTable.new_table_name) { + tablesNames[currTable.table_name] = { + table_name: currTable.table_name, + table_schema: currTable.table_schema, + new_table_name: currTable.new_table_name, + }; + } + } + // Populates the columnsNames object with new column names + function renameColumn(currTable: AlterTablesObjType): void { + for (let i = 0; i < currTable.alterColumns.length; i+=1) { + const currAlterColumn: AlterColumnsObjType = currTable.alterColumns[i]; + // populates an array of objects with all of the new column names + if (currAlterColumn.new_column_name) { + columnsNames[currAlterColumn.column_name] = { + column_name: currAlterColumn.column_name, + table_name: currTable.table_name, + table_schema: currTable.table_schema, + new_column_name: currAlterColumn.new_column_name, + }; + } + } + } + const renameConstraintCache = {} + // Populates the constraintsNAmes object with new constraint names + function renameConstraint(currTable): void { + for (let i = 0; i < currTable.alterColumns.length; i+=1) { + const currAlterColumn: AlterColumnsObjType = currTable.alterColumns[i]; + // populates an array of objects with all of the new constraint names + if (currAlterColumn.rename_constraint) { + constraintsNames[currAlterColumn.rename_constraint] = { + constraint_type: currAlterColumn.rename_constraint[0] === 'p' ? 'pk' : 'f' ? 'fk' : 'unique', + column_name: currAlterColumn.new_column_name ? currAlterColumn.new_column_name : currAlterColumn.column_name, + table_name: renameConstraintCache[currTable.table_name] ? renameConstraintCache[currTable.table_name] : currTable.table_name, + table_schema: currTable.table_schema, + }; + } + } + } + + for (let i = 0; i < renameTableArray.length; i+=1) { + if (renameTableArray[i].new_table_name) renameConstraintCache[renameTableArray[i].table_name] = renameTableArray[i].new_table_name; + } + + for (let i = 0; i < renameTableArray.length; i+=1) { + const currTable: AlterTablesObjType = renameTableArray[i]; + renameConstraint(currTable); + renameColumn(currTable); + renameTable(currTable); + } + // Goes through the columnsNames object and adds the query for renaming + const columnsToRename: string[] = Object.keys(columnsNames); + for (let i = 0; i < columnsToRename.length; i+=1) { + const currColumn: AlterColumnsObjType = columnsNames[columnsToRename[i]]; + // only renames a column with the most recent name that was saved + renameString += `ALTER TABLE ${currColumn.table_schema}.${currColumn.table_name} RENAME COLUMN ${currColumn.column_name} TO ${currColumn.new_column_name}; `; + } + // Goes through the tablesNames object and adds the query for renaming + const tablesToRename: string[] = Object.keys(tablesNames); + for (let i = 0; i < tablesToRename.length; i+=1) { + const currTable: AlterTablesObjType = tablesNames[tablesToRename[i]]; + // only renames a table with the most recent name that was saved + renameString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} RENAME TO ${currTable.new_table_name}; `; + } + // Goes through the constraintsNames object and adds the query for renaming + const constraintsToRename: string[] = Object.keys(constraintsNames); + for (let i = 0; i < constraintsToRename.length; i+=1) { + const currColumn: AlterColumnsObjType = constraintsNames[constraintsToRename[i]]; + renameString += `ALTER TABLE ${currColumn.table_schema}.${currColumn.table_name} RENAME CONSTRAINT ${constraintsToRename[i]} TO ${currColumn.constraint_type}_${currColumn.table_name}${currColumn.column_name}; `; + } + outputArray.push(renameString); + } + + addTable(backendObj.updates.addTables); + dropTable(backendObj.updates.dropTables); + alterTable(backendObj.updates.alterTables); + renameTablesColumns(backendObj.updates.alterTables); + return outputArray.join(''); +} + +export default backendObjToQuery; diff --git a/backend/main.ts b/backend/main.ts index ff24cae5..f9f77386 100644 --- a/backend/main.ts +++ b/backend/main.ts @@ -1,6 +1,8 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { FamilyRestroomRounded } from '@mui/icons-material'; -import { app, BrowserWindow, Menu } from 'electron'; +import { BrowserWindow, Menu } from 'electron'; + +const { app } = require('electron'); const dev: boolean = process.env.NODE_ENV === 'development'; @@ -61,17 +63,6 @@ function createWindow() { }); } -// Install React Dev Tools Extension -if (dev) { - const { - default: installExtension, - REACT_DEVELOPER_TOOLS, - } = require('electron-devtools-installer'); - - app.on('ready', () => { - installExtension(REACT_DEVELOPER_TOOLS); - }); -} // Invoke createWindow to create browser windows after Electron has been initialized. app.on('ready', createWindow); diff --git a/frontend/components/App.tsx b/frontend/components/App.tsx index 6934d688..9a384d88 100644 --- a/frontend/components/App.tsx +++ b/frontend/components/App.tsx @@ -33,7 +33,7 @@ const Main = styled.main<{ $fullwidth: boolean }>` grid-area: ${({ $fullwidth }) => ($fullwidth ? '1 / 1 / -1 / -1' : 'main')}; background: ${bgColor}; height: calc(100vh - (2 * ${defaultMargin})); - max-width: ${({ $fullwidth }) => ($fullwidth ? '' : `calc(100vw - ${sidebarWidth} )`)}; + max-width: ${({ $fullwidth }) => ($fullwidth ? '' : `calc(90vw - ${sidebarWidth} )`)}; padding: ${defaultMargin} ${sidebarShowButtonSize}; margin: 0; `; @@ -51,6 +51,8 @@ const App = () => { const [selectedDb, setSelectedDb] = useState(''); const [sidebarIsHidden, setSidebarHidden] = useState(false); const [newFilePath, setFilePath] = useState(''); + const [ERView, setERView] = useState(true); + /** * Hook to create new Query from data @@ -117,7 +119,8 @@ const App = () => { setSidebarHidden, sidebarIsHidden, setFilePath, - newFilePath + newFilePath, + setERView }} />
@@ -125,7 +128,12 @@ const App = () => { queries={comparedQueries} show={shownView === 'compareView'} /> - + { +}: DbListProps) => { const [databases, setDatabases] = useState([]); const [openAdd, setOpenAdd] = useState(false); const [openDupe, setOpenDupe] = useState(false); diff --git a/frontend/components/sidebar/Sidebar.tsx b/frontend/components/sidebar/Sidebar.tsx index 4d91f4fe..a841e029 100644 --- a/frontend/components/sidebar/Sidebar.tsx +++ b/frontend/components/sidebar/Sidebar.tsx @@ -63,6 +63,7 @@ const Sidebar = ({ sidebarIsHidden, setFilePath, newFilePath, + setERView }: AppState) => { const toggleOpen = () => setSidebarHidden(!sidebarIsHidden); @@ -88,7 +89,7 @@ const Sidebar = ({ setSelectedView={setSelectedView} toggleOpen={toggleOpen} /> - + ; +type ViewSelectorProps = Pick; /** * Selector for view on sidebar. Updates App state with selected view */ -const ViewSelector = ({ selectedView, setSelectedView }: ViewSelectorProps) => ( +const ViewSelector = ({ selectedView, setSelectedView, setERView }: ViewSelectorProps) => ( setSelectedView('queryView')} @@ -37,7 +37,10 @@ const ViewSelector = ({ selectedView, setSelectedView }: ViewSelectorProps) => ( Queries setSelectedView('dbView')} + onClick={() => { + setSelectedView('dbView') + if (setERView) setERView(true) + }} $isSelected={ selectedView === 'dbView' || selectedView === 'quickStartView' } diff --git a/frontend/components/views/DbView/DatabaseDetails.tsx b/frontend/components/views/DbView/DatabaseDetails.tsx index 48b3ef9c..bacb74d3 100644 --- a/frontend/components/views/DbView/DatabaseDetails.tsx +++ b/frontend/components/views/DbView/DatabaseDetails.tsx @@ -17,10 +17,9 @@ const DatabaseDetails = ({ db }: DatabaseDetailsProps) => { if (!db) return null; return ( - {`${db.db_name}`} - {`${db.db_size}`} + {`${db.db_name}`} {`${db.db_size}`} ); }; -export default DatabaseDetails; +export default DatabaseDetails; \ No newline at end of file diff --git a/frontend/components/views/DbView/DbView.tsx b/frontend/components/views/DbView/DbView.tsx index 48ab4bce..9d635bac 100644 --- a/frontend/components/views/DbView/DbView.tsx +++ b/frontend/components/views/DbView/DbView.tsx @@ -14,6 +14,8 @@ const requestDbListOnce = once(() => ipcRenderer.send('return-db-list')); interface DbViewProps { selectedDb: AppState['selectedDb']; show: boolean; + setERView: (boolean) => void; + ERView: boolean; } const StyledDummyButton = styled(Button)` @@ -22,7 +24,7 @@ const StyledDummyButton = styled(Button)` right: ${sidebarShowButtonSize}; `; -const DbView = ({ selectedDb, show }: DbViewProps) => { +const DbView = ({ selectedDb, show, setERView, ERView}: DbViewProps) => { const [dbTables, setTables] = useState([]); const [selectedTable, setSelectedTable] = useState(); const [databases, setDatabases] = useState([]); @@ -65,10 +67,12 @@ const DbView = ({ selectedDb, show }: DbViewProps) => { tables={dbTables} selectTable={(table: TableInfo) => setSelectedTable(table)} selectedTable={selectedTable} + selectedDb={selectedDb} + setERView={setERView} />

- {selectedTable ? ( + {(selectedTable && !ERView) ? ( ({ 'aria-controls': `scrollable-auto-tabpanel-${index}`, }); -interface TablesTabBarProps { - +interface TablesTabBarProps { tables: TableInfo[]; selectTable: (table: TableInfo) => void; selectedTable: TableInfo | undefined; + selectedDb: AppState['selectedDb']; + setERView?: (boolean) => void; } const TablesTabs = ({ tables, selectTable, selectedTable, + selectedDb, + setERView, }: TablesTabBarProps) => { - - - - const handleChange = (event: React.ChangeEvent<{}>, newValue: number) => { selectTable(tables[newValue]); }; @@ -56,30 +60,81 @@ const TablesTabs = ({ const tableIndex = tables.findIndex( ({ table_name }) => table_name === selectedTable?.table_name ); + + const [active, setActive] = useState(true); + + const ErView = () => ( +
+ { active ? ( + + ) : ( + <> + + {tables.map(({ table_name: name }, index) => ( + + ))} + ; + +
+
+ {tables.map((tableMap, index) => ( + + + + ))} + + )} +
+ ); + + + const handleView = (e ,newActive) => { + // force at least one selected view + if (newActive !== null) { + // set the new view to the currect view + setActive(newActive); + + // disable the dummy data button when in ER View + if (setERView) { + if (active) setERView(newActive); + else setERView(newActive); + }; + } + }; + + const StyledToggleButtonGroup = styled(ToggleButtonGroup)` + background-color: ${greenPrimary}; + margin-bottom: 10px; + `; return ( - <> - + - {tables.map(({ table_name: name }, index) => ( - - ))} - ; - -
-
- {tables.map((tableMap, index) => ( - - - - ))} - + + ER diagram + + + Table + + + {ErView()} +
); }; diff --git a/frontend/components/views/DbView/sample-updateschema.js b/frontend/components/views/DbView/sample-updateschema.js new file mode 100644 index 00000000..f84bdace --- /dev/null +++ b/frontend/components/views/DbView/sample-updateschema.js @@ -0,0 +1,137 @@ +const updateSchema = +{ + database: "starwars", + updates: { + addTables: [ + { + is_insertable_into: "YES", + table_catalog: "starwars", + table_name: "people", + table_schema: "public" + }, + { + is_insertable_into: "YES", + table_catalog: "starwars", + table_name: "people_in_films", + table_schema: "public" + }, + { + is_insertable_into: "YES", + table_catalog: "starwars", + table_name: "testdrop", + table_schema: "public" + }, + ], + dropTables: [ + { + table_name: "testdrop", + table_schema: "public" + } + ], + alterTables: [ + { + is_insertable_into: "YES", + table_catalog: "starwars", + table_name: "people", + new_table_name: "people_new", + table_schema: "public", + addColumns: [ + { + column_name: "mass", + data_type: "SERIAL" + }, + ], + dropColumns: [ + ], + alterColumns: [ + { + character_maximum_length: 20, + column_name: "mass", + new_column_name: "mass_new", + add_constraint: [ + { + constraint_type: "PRIMARY KEY", + constraint_name: "pk2", + foreign_table: null, + foreign_column: null, + }, + ], + data_type: null, + is_nullable: "NO", + drop_constraint: [] + }, + ], + }, + { + is_insertable_into: "YES", + table_catalog: "starwars", + table_name: "people_in_films", + new_table_name: "people_in_starwars", + table_schema: "public", + addColumns: [ + { + column_name: "id", + data_type: "SERIAL" + }, + { + column_name: "person_name", + data_type: "INTEGER" + }, + { + column_name: "name1", + data_type: "VARCHAR" + }, + ], + dropColumns: [ + { + column_name: "name1" + }, + ], + alterColumns: [ + { + character_maximum_length: 20, + column_name: "id", + new_column_name: "id_new", + add_constraint: [ + { + constraint_type: "PRIMARY KEY", + constraint_name: "pk", + foreign_table: null, + foreign_column: null, + }, + { + constraint_type: "UNIQUE", + constraint_name: "unique_1", + foreign_table: null, + foreign_column: null, + }, + ], + data_type: null, + is_nullable: "NO", + drop_constraint: [] + }, + { + character_maximum_length: 20, + column_name: "person_name", + constraint_name: "mass_fk0", + new_column_name: "person_new_name", + add_constraint: [ + { + constraint_type: "FOREIGN KEY", + constraint_name: "fk000", + foreign_table: "people", + foreign_column: "mass", + unique: null, + }, + ], + data_type: null, + is_nullable: "YES", + drop_constraint: [] + }, + ], + }, + ], + } +} + +export default updateSchema; \ No newline at end of file diff --git a/frontend/components/views/ERTables/ERTabling.tsx b/frontend/components/views/ERTables/ERTabling.tsx new file mode 100644 index 00000000..98eea89b --- /dev/null +++ b/frontend/components/views/ERTables/ERTabling.tsx @@ -0,0 +1,274 @@ +import fs from 'fs'; +import { ipcRenderer, remote } from 'electron'; +import React, { useCallback, useEffect, useState, useRef } from 'react'; +import ReactFlow, { + applyEdgeChanges, + applyNodeChanges, + Background, + Node, + Edge, + MiniMap, +} from 'react-flow-renderer'; +import { Button } from '@material-ui/core'; +import styled from 'styled-components'; +import stateToReactFlow from '../../../lib/convertStateToReactFlow'; +import nodeTypes from './NodeTypes'; +import { + UpdatesObjType, + AddTablesObjType, + TableHeaderNodeType, + AppState, + SchemaStateObjType, +} from '../../../types'; + +import * as colors from '../../../style-variables'; + +// defines the styling for the ERDiagram window +const rfStyle: object = { + height: '65vh', +}; + +// defines the styling for the minimap +const mmStyle: object = { + backgroundColor: colors.bgColor, + border: `2px solid ${colors.greenPrimary}`, + 'borderRadius': '0.3rem', +}; + +// defines the styling for the minimap nodes +const nodeColor = (node): string => { + switch (node.type) { + case 'tableHeader': + return colors.greyLightest; + case 'tableField': + return 'white'; + default: + return 'red'; + } +}; + +type ERTablingProps = { + tables; + selectedDb: AppState['selectedDb']; +}; + +const StyledViewButton = styled(Button)` + margin: 1rem; + margin-left: 0rem; + font-size: .78em; + padding: .45em; +`; + +function ERTabling({ tables, selectedDb }: ERTablingProps) { + const [schemaState, setSchemaState] = useState({ + database: 'initial', + tableList: [], + }); + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + // when tables (which is the database that is selected changes, update SchemaState) + useEffect(() => { + setSchemaState({ database: selectedDb, tableList: tables }); + }, [tables, selectedDb]); + // define an object using the useRef hook to maintain its value throughout all rerenders + // this object will hold the data that needs to get sent to the backend to update the + // SQL database. Each node will have access to this backendObj + const updates: UpdatesObjType = { + addTables: [], + dropTables: [], + alterTables: [], + }; + const backendObj = useRef({ + database: schemaState.database, + updates, + }); + useEffect(() => { + backendObj.current.database = selectedDb; + }, [selectedDb]); + + const backendColumnObj = useRef({ + database: schemaState.database, + updates, + }); + // whenever the selectedDb changes, reassign the backendObj to contain this selectedDb + useEffect(() => { + backendObj.current.database = selectedDb; + backendColumnObj.current.database = selectedDb; + }, [selectedDb]); + + // This useEffect fires when schemaState changes and will convert the state to a form react flow requires + useEffect(() => { + // send the schema state to the convert method to convert the schema to the form react flow requires + const initialState = stateToReactFlow.convert(schemaState); + // create a deep copy of the state, to ensure the state is not directly modified + const schemaStateString = JSON.stringify(schemaState); + const schemaStateCopy = JSON.parse(schemaStateString); + // create a nodesArray with the initialState data + const nodesArray = initialState.nodes.map((currentNode) => { + // add the schemaStateCopy and setSchemaState to the nodes data so that each node + // has reference to the current state and can modify the state to cause rerenders + const { data } = currentNode; + return { + ...currentNode, + data: { + ...data, + schemaStateCopy, + setSchemaState, + backendObj, + handleClickSave, + }, + }; + }); + setNodes(nodesArray); + setEdges(initialState.edges); + }, [schemaState]); + + // whenever the node changes, this callback gets invoked + const onNodesChange = useCallback( + (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), + [setNodes] + ); + // whenever the edges changes, this callback gets invoked + const onEdgesChange = useCallback( + (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), + [setEdges] + ); + + // This function handles the add table button on the ER Diagram view + const handleAddTable = (): void => { + const schemaStateString = JSON.stringify(schemaState); + const schemaStateCopy = JSON.parse(schemaStateString); + // create an addTablesType object with AddTablesObjType + const addTableObj: AddTablesObjType = { + is_insertable_into: 'yes', + table_name: `NewTable${schemaStateCopy.tableList.length + 1}`, + table_schema: `public`, + table_catalog: `${schemaStateCopy.database}`, + columns: [], + }; + // update the backendObj + backendObj.current.updates.addTables.push(addTableObj); + // push a new object with blank properties + schemaStateCopy.tableList.push(addTableObj); + // set the state + setSchemaState(schemaStateCopy); + }; + + const handleSaveLayout = (): void => { + // get the array of header nodes + const headerNodes = nodes.filter( + (node) => node.type === 'tableHeader' + ) as TableHeaderNodeType[]; + + // create object for the current database + type TablePosObjType = { + table_name: string; + table_position: { + x: number; + y: number; + }; + }; + + type DatabaseLayoutObjType = { + db_name: string; + db_tables: TablePosObjType[]; + }; + + const currDatabaseLayout: DatabaseLayoutObjType = { + db_name: backendObj.current.database, + db_tables: [], + }; + + // populate the db_tables property for the database + headerNodes.forEach((node) => { + const tablePosObj: TablePosObjType = { + table_name: node.tableName, + table_position: { x: node.position.x, y: node.position.y }, + }; + currDatabaseLayout.db_tables.push(tablePosObj); + }); + + const location: string = remote.app.getPath('temp').concat('/UserTableLayouts.json'); + fs.readFile(location, 'utf-8', (err, data) => { + // check if error exists (no file found) + if (err) { + fs.writeFile( + location, + JSON.stringify([currDatabaseLayout], null, 2), + (error) => { + if (error) console.log(error); + } + ); + + // check if file exists + } else { + const dbLayouts = JSON.parse(data) as DatabaseLayoutObjType[]; + let dbExists = false; + // if db has saved layout settings overwrite them + dbLayouts.forEach((db, i) => { + if (db.db_name === currDatabaseLayout.db_name) { + dbLayouts[i] = currDatabaseLayout; + dbExists = true; + } + }); + // if db has no saved layout settings add to file + if (!dbExists) dbLayouts.push(currDatabaseLayout); + + // write changes to the file + fs.writeFile(location, JSON.stringify(dbLayouts, null, 2), (error) => { + if (error) console.log(error); + }); + } + }); + }; + const handleClickSave = (): void => { + // This function sends a message to the back end with + // the data in backendObj.current + handleSaveLayout(); + ipcRenderer + .invoke('ertable-schemaupdate', backendObj.current) + .then(async () => { + // resets the backendObj + backendObj.current = { + database: schemaState.database, + updates, + }; + }) + .catch((err: object) => { + console.log(err); + }); + }; + + return ( +
+ + {' '} + Add New Table{' '} + + + {' '} + Save{' '} + + + + + +
+ ); +} + +export default ERTabling; diff --git a/frontend/components/views/ERTables/NodeTypes.ts b/frontend/components/views/ERTables/NodeTypes.ts new file mode 100644 index 00000000..5287dcc5 --- /dev/null +++ b/frontend/components/views/ERTables/NodeTypes.ts @@ -0,0 +1,22 @@ +import tableHeader from './TableHeaderNode'; +import tableField from './TableFieldNode'; +/** + * This file is required for React-flow + * React-flow states: + * " You can add a new node type to React Flow by adding it to the nodeTypes prop. + * It's important that the nodeTypes are memoized or defined outside of the component. + * Otherwise React creates a new object on every render which leads to performance issues and bugs." + * + * https://reactflow.dev/docs/guides/custom-nodes/ + * + */ +type NodeTypes = { + tableHeader: any + tableField: any +} +const nodeTypes: NodeTypes = { + tableHeader, + tableField +} + +export default nodeTypes; \ No newline at end of file diff --git a/frontend/components/views/ERTables/TableFieldCheckBox.tsx b/frontend/components/views/ERTables/TableFieldCheckBox.tsx new file mode 100644 index 00000000..9a61b122 --- /dev/null +++ b/frontend/components/views/ERTables/TableFieldCheckBox.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +type TableFieldCheckBoxProps = { + label : string; + idName : string; + isChecked? : boolean | undefined; + onChange? : ((any) => void)[] | ((any) => void); +} + +const TableFieldCheckBox = ({label, idName, isChecked, onChange}: TableFieldCheckBoxProps) => { + const onChangeHandler = (e) => { + // confirm that onChange is given + if (!onChange) return; + // if an array of callbacks is given, apply consecutively + if (Array.isArray(onChange)) { + onChange.forEach(func => func(e.target.checked)) + } else { + // if only one callback is given, apply solely + onChange(e.target.checked) + } + } + + return ( +
+ {`${label}:`} +
+ {onChangeHandler(e)}} + /> +
+
+ ) +} + + +export default TableFieldCheckBox; \ No newline at end of file diff --git a/frontend/components/views/ERTables/TableFieldDropDown.tsx b/frontend/components/views/ERTables/TableFieldDropDown.tsx new file mode 100644 index 00000000..e1844de7 --- /dev/null +++ b/frontend/components/views/ERTables/TableFieldDropDown.tsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from 'react'; +import TableFieldDropDownOption from './TableFieldDropDownOption'; + +type OtherTablesType = { + table_name: string; + column_names: string[]; +}; + +type TableFieldDropDownProps = { + label: string; + idName: string; + options: string[]; + defaultValue: string; + isDisabled?: boolean; + otherTables: OtherTablesType[]; + setFkOptions?: (fkOptions: string[]) => void; + schemaStateCopy: any; + setSchemaState: (string) => {}; +}; + +const TableFieldDropDown = (props: TableFieldDropDownProps) => { + const { + label, + idName, + options, + defaultValue, + isDisabled, + otherTables, + setFkOptions, + schemaStateCopy, + setSchemaState, + } = props; + + const optionsArray = options.map((option, i) => ( + + )); + + const handleChange = (e) => { + // if setFKOptions is truthy, the instance of TableFieldDropDown + // with Other table names has been changed + if (setFkOptions) { + // check to see if otherTables is truthy + // set the FK options to rerender a new list depending on the table name + const newTableFkOptions = otherTables.find( + (el) => el.table_name === e.target.value + ); + + if (newTableFkOptions) { + setFkOptions(newTableFkOptions.column_names); + } + } + // setSchemaState(schemaStateCopy) + }; + + return ( +
+ {`${label}:`} + +
+ ); +}; + +export default TableFieldDropDown; diff --git a/frontend/components/views/ERTables/TableFieldDropDownOption.tsx b/frontend/components/views/ERTables/TableFieldDropDownOption.tsx new file mode 100644 index 00000000..785ee9d8 --- /dev/null +++ b/frontend/components/views/ERTables/TableFieldDropDownOption.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +type TableFieldDropDownOptionProps = { + idName: string; + option: string; +} + +const TableFieldDropDownOption = (props: TableFieldDropDownOptionProps) => { + const { idName, option } = props; + + return ( + + ); +} + +export default TableFieldDropDownOption; \ No newline at end of file diff --git a/frontend/components/views/ERTables/TableFieldInput.tsx b/frontend/components/views/ERTables/TableFieldInput.tsx new file mode 100644 index 00000000..c7d909cb --- /dev/null +++ b/frontend/components/views/ERTables/TableFieldInput.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +type TableFieldInputProps = { + defaultValue: any; + label: string; + idName: string; +}; +const TableFieldInput = ({ + defaultValue, + label, + idName, +}: TableFieldInputProps) => { + return ( +
+ {`${label}:`} + +
+ ); +}; + +export default TableFieldInput; diff --git a/frontend/components/views/ERTables/TableFieldNode.tsx b/frontend/components/views/ERTables/TableFieldNode.tsx new file mode 100644 index 00000000..89d6bf06 --- /dev/null +++ b/frontend/components/views/ERTables/TableFieldNode.tsx @@ -0,0 +1,503 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Handle, Position } from 'react-flow-renderer'; +import { AccordionSummary, AccordionDetails } from '@mui/material'; +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; +import styled from 'styled-components'; +import { Button } from '@material-ui/core'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { + ERTableColumnData, + BackendObjType, + AlterTablesObjType, + DropColumnsObjType, + AlterColumnsObjType, + AddConstraintObjType, +} from '../../../types'; +import TableFieldCheckBox from './TableFieldCheckBox'; +import TableFieldInput from './TableFieldInput'; +import TableFieldDropDown from './TableFieldDropDown'; +import './styles.css'; + +type TableFieldDataObjectType = { + table_name: string; + schemaStateCopy: any; + setSchemaState: (string) => {}; + backendObj: BackendObjType; +}; + +type TableFieldProps = { + data; +}; + +const StyledButton = styled(Button)` + color: rgba(0, 0, 0, 0.87); + background-color: rgb(203, 212, 214); + margin: 1rem; + margin-left: 0rem; + padding: 0.5rem 1rem; +`; + +// Mui wrapper is used here to use the default styling +// the imported MuiAccordion component comes with +const Accordion = styled((props: AccordionProps) => ( + +))(() => ({})); + +function TableField({ data }: TableFieldProps) { + const { + schemaStateCopy, + setSchemaState, + backendObj, + }: TableFieldDataObjectType = data; + const { + is_nullable, + constraint_type, + column_name, + data_type, + character_maximum_length, + foreign_column, + foreign_table, + }: ERTableColumnData = data.columnData; + + const tableColumn: string = `${data.tableName}-${column_name}`; + const isNull: string = is_nullable; + let setTimeoutVariable; + + // handles functionality of the drop down delete button + const handleDropColumn = (): void => { + // iterate through schema copy + for (let i = 0; i < schemaStateCopy.tableList.length; i += 1) { + // edit schema table for this current table + if (schemaStateCopy.tableList[i].table_name === data.tableName) { + let columnIndex: number; + // iterate through columns + for ( + let j: number = 0; + j < schemaStateCopy.tableList[i].columns.length; + j += 1 + ) { + if ( + schemaStateCopy.tableList[i].columns[j].column_name === column_name + ) { + columnIndex = j; + + // create alterTablesObject with AlterTablesObjecttype + const alterTablesObj: AlterTablesObjType = { + is_insertable_into: null, + table_catalog: schemaStateCopy.tableList[i].table_catalog, + table_name: schemaStateCopy.tableList[i].table_name, + new_table_name: null, + table_schema: schemaStateCopy.tableList[i].table_schema, + addColumns: [], + dropColumns: [], + alterColumns: [], + }; + // create a deleteColumnsType object + const dropColumnObj: DropColumnsObjType = { + column_name: schemaStateCopy.tableList[i].columns[j].column_name, + }; + // add deleteColumns obj to the alterTablesObj + alterTablesObj.dropColumns.push(dropColumnObj); + // update the backendObj + backendObj.current.updates.alterTables.push(alterTablesObj); + // alter schema state to remove the column + schemaStateCopy.tableList[i].columns.splice(columnIndex, 1); + // set the state + setSchemaState(schemaStateCopy); + return; + } + } + } + } + }; + // handles functionality of the drop down update button + const handleUpdateColumn = (): void => { + // create an alterColumns object + const alterColumnsObj: AlterColumnsObjType = { + column_name, + character_maximum_length: null, + new_column_name: null, + add_constraint: [], + current_data_type: data_type, + data_type: null, + is_nullable: null, + drop_constraint: [], + rename_constraint: null, + table_schema: null, + table_name: null, + constraint_type: null + }; + + for (let i = 0; i < schemaStateCopy.tableList.length; i += 1) { + if (schemaStateCopy.tableList[i].table_name === data.tableName) { + // iterate through columns + for ( + let j: number = 0; + j < schemaStateCopy.tableList[i].columns.length; + j += 1 + ) { + if ( + schemaStateCopy.tableList[i].columns[j].column_name === column_name + ) { + const alterTablesObj: AlterTablesObjType = { + is_insertable_into: null, + table_catalog: schemaStateCopy.tableList[i].table_catalog, + table_name: data.tableName, + new_table_name: null, + table_schema: schemaStateCopy.tableList[i].table_schema, + addColumns: [], + dropColumns: [], + alterColumns: [], + }; + + // handle column_name change + const columnNameInput = document.getElementById( + `type-input-column_name-${tableColumn}` + ) as HTMLSelectElement; + if (column_name !== columnNameInput.value) { + alterColumnsObj.new_column_name = columnNameInput.value; + schemaStateCopy.tableList[i].columns[j].new_column_name = + columnNameInput.value; + if (constraint_type === 'PRIMARY KEY') alterColumnsObj.rename_constraint = `pk_${alterTablesObj.table_name}${column_name}`; + if (constraint_type === 'FOREIGN KEY') alterColumnsObj.rename_constraint = `fk_${alterTablesObj.table_name}${column_name}`; + if (constraint_type === 'UNIQUE') alterColumnsObj.rename_constraint = `unique_${alterTablesObj.table_name}${column_name}`; + } + + // handle isNullable change + const isNullable = document.getElementById( + `allow-null-chkbox-${tableColumn}` + ) as HTMLInputElement; + const isNullableString: 'YES' | 'NO' = isNullable.checked ? 'YES' : 'NO'; + alterColumnsObj.is_nullable = + isNull !== isNullableString ? isNullableString : null; + + // handle max_character_length change + const columnMaxCharacterLengthInput = document.getElementById( + `type-input-char_max_size-${tableColumn}` + ) as HTMLSelectElement; + if (columnMaxCharacterLengthInput.value) { + if ( + character_maximum_length !== + parseInt(columnMaxCharacterLengthInput.value, 10) + ) { + alterColumnsObj.character_maximum_length = parseInt( + columnMaxCharacterLengthInput.value, + 10 + ); + schemaStateCopy.tableList[i].columns[ + j + ].character_maximum_length = parseInt( + columnMaxCharacterLengthInput.value, + 10 + ); + } + } + + // handle data_type change + const dataTypeInput = document.getElementById( + `type-dd-${tableColumn}` + ) as HTMLSelectElement; + if ( + (data_type === 'character varying' ? 'varchar' : data_type) !== + dataTypeInput.value + ) { + alterColumnsObj.data_type = dataTypeInput.value; + schemaStateCopy.tableList[i].columns[j].data_type = + dataTypeInput.value; + } + + // handle add/Drop Constraint type + // create an empty AddConstraintObj + const addConstraintObj: AddConstraintObjType = { + constraint_type: null, + constraint_name: '', + foreign_table: null, + foreign_column: null, + }; + /* handle primary key */ + // get the primary key checkmark value + const pkCheckBox = document.getElementById( + `primary-key-chkbox-${tableColumn}` + ) as HTMLInputElement; + // if constraint type is PK in state but checkbox is unchecked, drop the constraint + if ( + constraint_type === 'PRIMARY KEY' && + pkCheckBox.checked === false + ) { + // add the PK constraint name to the drop constraint array + alterColumnsObj.drop_constraint.push( + `PK_${data.tableName + column_name}` + ); + } // if constraint type is not in state but checkbox is checked, add the constraint + else if ( + constraint_type !== 'PRIMARY KEY' && + pkCheckBox.checked === true + ) { + // create a copy in case multiple constraints are added + const addConstraintObjCopy: AddConstraintObjType = { ...addConstraintObj }; + // name the constraint PK_ + addConstraintObjCopy.constraint_name = `pk_${data.tableName + column_name + }`; + // assign the constraint_type to 'PRIMARY KEY' + addConstraintObjCopy.constraint_type = 'PRIMARY KEY'; + // add the constraint obj to the alter columns obj + alterColumnsObj.add_constraint.push(addConstraintObjCopy); + } + + // handle foreign key + const fkCheckBox = document.getElementById( + `foreign-key-chkbox-${tableColumn}` + ) as HTMLInputElement; + // if constraint type is FK in state but checkbox is unchecked, drop the constraint + if ( + constraint_type === 'FOREIGN KEY' && + fkCheckBox.checked === false + ) { + // add the fk constraint name to the drop constraint array + alterColumnsObj.drop_constraint.push( + `FK_${data.tableName + column_name}` + ); + } else if ( + constraint_type !== 'FOREIGN KEY' && + fkCheckBox.checked === true + ) { + const addConstraintObjCopy = { ...addConstraintObj }; + // name the constraint FK_ + addConstraintObjCopy.constraint_name = `fk_${data.tableName + column_name + }`; + // assign the constraint type to 'FOREIGN KEY' + addConstraintObjCopy.constraint_type = 'FOREIGN KEY'; + // get the value of the drop down for foreign table + const foreignTableDD = document.getElementById( + `foreign-key-table-dd-${tableColumn}` + ) as HTMLSelectElement; + // assign the constraintobjcopy to foreign table value + addConstraintObjCopy.foreign_table = foreignTableDD.value; + // get the value of the drop down for foreign column + const foreignColumnDD = document.getElementById( + `foreign-key-field-dd-${tableColumn}` + ) as HTMLSelectElement; + // assign the constraintobjcopy to foreign column value + addConstraintObjCopy.foreign_column = foreignColumnDD.value; + // add the constraint obj to the alter columns obj + alterColumnsObj.add_constraint.push(addConstraintObjCopy); + } + + // handle unique constraint + const uniqueCheckBox = document.getElementById(`unique-chkbox-${tableColumn}`) as HTMLInputElement; + if (constraint_type === 'UNIQUE' && uniqueCheckBox.checked === false) { + // add the unique constraint name to the drop constraint array + alterColumnsObj.drop_constraint.push( + `unique_${data.tableName + column_name}` + ); + } + else if (constraint_type !== 'UNIQUE' && uniqueCheckBox.checked === true) { + // create a copy in case multiple constraints are added + const addConstraintObjCopy: AddConstraintObjType = { ...addConstraintObj }; + // name the constraint PK_ + addConstraintObjCopy.constraint_name = `unique_${data.tableName + column_name + }`; + // assign the constraint_type to 'UNIQUE' + addConstraintObjCopy.constraint_type = 'UNIQUE'; + // add the constraint obj to the alter columns obj add_constraint array + alterColumnsObj.add_constraint.push(addConstraintObjCopy); + } + + // add the alterTablesObj + alterTablesObj.alterColumns.push(alterColumnsObj); + // update the backendObj + backendObj.current.updates.alterTables.push(alterTablesObj); + setSchemaState(schemaStateCopy); + return; + } + } + } + } + }; + + // autopopulates the fk field options from state + const createFieldOptions = (): string[] => { + const options: string[] = []; + + // if foreign_table is NOT provided return column names of first table in otherTables + if (foreign_table == null && data.otherTables.length > 0) { + options.push(...data.otherTables[0].column_names); + } + + // if foreign_table is provided return associated column_names + data.otherTables.forEach((table) => { + if (table.table_name === foreign_table) { + options.push(...table.column_names); + } + }); + + return options; + }; + + // disable the dropdown menus for fk table and field when fk checkbox is not checked + const disableForeignKeyMenuHandler = (isChecked) => { + const tableID: string = `foreign-key-table-dd-${tableColumn}`; + const fieldID: string = `foreign-key-field-dd-${tableColumn}`; + + const tableDD = document.getElementById(tableID) as HTMLSelectElement; + const fieldDD = document.getElementById(fieldID) as HTMLSelectElement; + + tableDD.disabled = !isChecked; + fieldDD.disabled = !isChecked; + }; + + // disable allow null checkbox when the column is either a foreign key or primary key + const disableAllowNullHandler = () => { + const pkID: string = `primary-key-chkbox-${tableColumn}`; + const pkCheckBox = document.getElementById(pkID) as HTMLInputElement; + const isPkChecked: boolean = pkCheckBox.checked; + + const fkID: string = `foreign-key-chkbox-${tableColumn}`; + const fkCheckBox = document.getElementById(fkID) as HTMLInputElement; + const isFkChecked: boolean = fkCheckBox.checked; + + const allowNullID: string = `allow-null-chkbox-${tableColumn}`; + const allowNullCheckBox = document.getElementById( + allowNullID + ) as HTMLSelectElement; + allowNullCheckBox.disabled = isFkChecked || isPkChecked; + }; + + // create a state for the foreign key drop down options + const [fkOptions, setFkOptions] = useState(createFieldOptions()); + + const [isAccordionExpanded, setAccordionExpanded] = useState(false); + + // This function handles the click functionality of clicking the accordion + const handleAccordionClick = (): void => { + setAccordionExpanded(!isAccordionExpanded); + }; + + // This function closes the accordion expansion on mouse leave + const handleMouseLeave = (): void => { + setTimeoutVariable = setTimeout(() => { + setAccordionExpanded(false); + }, 1000); + }; + // This function clears the timeout if the mouse reenters + // within the timeout time + const handleMouseEnter = (): void => { + if (setTimeoutVariable) { + clearTimeout(setTimeoutVariable); + } + }; + + return ( +
+ {constraint_type === 'PRIMARY KEY' ? ( + + ) : ( + + )} + + } + > +
+

{column_name}

+

+ {data_type === 'character varying' ? 'varchar' : data_type} +

+
+
+ + + + +
+ + table.table_name)} + setFkOptions={setFkOptions} + otherTables={data.otherTables} + schemaStateCopy={schemaStateCopy} + setSchemaState={setSchemaState} + /> + +
+ + + +
+
+ + SAVE + + + DELETE + +
+
+
+
+ ); +} + +export default TableField; diff --git a/frontend/components/views/ERTables/TableHeaderNode.tsx b/frontend/components/views/ERTables/TableHeaderNode.tsx new file mode 100644 index 00000000..007a6a46 --- /dev/null +++ b/frontend/components/views/ERTables/TableHeaderNode.tsx @@ -0,0 +1,183 @@ +import React from 'react'; +import Tooltip from '@mui/material/Tooltip'; +import DeleteIcon from '@mui/icons-material/Delete'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import { + AlterTablesObjType, + AddColumnsObjType, + DropTablesObjType, + TableHeaderDataObjectType, + AlterColumnsObjType, +} from '../../../types'; +import './styles.css'; +import * as colors from '../../../style-variables'; + +type TableHeaderProps = { + data: TableHeaderDataObjectType; +}; + +function TableHeader({ data }: TableHeaderProps) { + const { table_name, schemaStateCopy, setSchemaState, backendObj } = data; + + // This function handles the add column button on the table + const handleAddColumn = (): void => { + // iterate through the schema copy + for (let i = 0; i < schemaStateCopy.tableList.length; i += 1) { + // edit the schema table for this current table + if (schemaStateCopy.tableList[i].table_name === table_name) { + // create an alterTableObject with AlterTablesObjectType + const alterTablesObj: AlterTablesObjType = { + is_insertable_into: null, + table_catalog: schemaStateCopy.tableList[i].table_catalog, + table_name, + new_table_name: null, + table_schema: schemaStateCopy.tableList[i].table_schema, + addColumns: [], + dropColumns: [], + alterColumns: [], + }; + + // create an addColumnsType object + const addColumnsObj: AddColumnsObjType = { + column_name: `NewColumn${ + schemaStateCopy.tableList[i].columns.length + 1 + }`, + data_type: 'varchar', + character_maximum_length: 255, + }; + // add the addColumnsObj to the alterTablesObj + alterTablesObj.addColumns.push(addColumnsObj); + // update the backendObj + backendObj.current.updates.alterTables.push(alterTablesObj); + // push a new object with blank properties + schemaStateCopy.tableList[i].columns.push({ + column_name: `NewColumn${ + schemaStateCopy.tableList[i].columns.length + 1 + }`, + new_column_name: `NewColumn${ + schemaStateCopy.tableList[i].columns.length + 1 + }`, + constraint_name: null, + constraint_type: null, + data_type: 'varchar', + character_maximum_length: 255, + foreign_column: null, + foreign_table: null, + is_nullable: 'NO', + }); + // set the state with the modified copy + setSchemaState(schemaStateCopy); + return; + } + } + }; + + // This function handles the add delete button on the table + const handleDeleteTable = (): void => { + for (let i = 0; i < schemaStateCopy.tableList.length; i += 1) { + if (schemaStateCopy.tableList[i].table_name === table_name) { + // create a dropTables Obj + const dropTablesObj: DropTablesObjType = { + table_name, + table_schema: schemaStateCopy.tableList[i].table_schema, + }; + // update backendObj + backendObj.current.updates.dropTables.push(dropTablesObj); + // update frontend + schemaStateCopy.tableList.splice(i, 1); + // set the state with the modified copy + setSchemaState(schemaStateCopy); + } + } + }; + + // This function updates the table name when the user hits enter on the submit form + const handleChangeTableName = (e): void => { + if (e.key === 'Enter') { + for (let i = 0; i < schemaStateCopy.tableList.length; i += 1) { + if (schemaStateCopy.tableList[i].table_name === table_name) { + const tableInputField = document.getElementById( + `table-name-form-${data.table_name}` + ) as HTMLInputElement; + + // update backend + const alterColumnsArray: AlterColumnsObjType[] = []; + for (let j = 0; j < schemaStateCopy.tableList[i].columns.length; j += 1) { + const alterColumnsObj: AlterColumnsObjType = { + column_name: schemaStateCopy.tableList[i].columns[j].column_name, + character_maximum_length: null, + new_column_name: null, + add_constraint: [], + current_data_type: null, + data_type: null, + is_nullable: null, + drop_constraint: [], + rename_constraint: null, + table_schema: null, + table_name: null, + constraint_type: null + } + if (schemaStateCopy.tableList[i].columns[j].constraint_type === 'PRIMARY KEY') { + alterColumnsObj.rename_constraint = `pk_${schemaStateCopy.tableList[i].table_name}${schemaStateCopy.tableList[i].columns[j].column_name}`; + alterColumnsArray.push(alterColumnsObj); + } + if (schemaStateCopy.tableList[i].columns[j].constraint_type === 'FOREIGN KEY') { + alterColumnsObj.rename_constraint = `fk_${schemaStateCopy.tableList[i].table_name}${schemaStateCopy.tableList[i].columns[j].column_name}`; + alterColumnsArray.push(alterColumnsObj); + } + if (schemaStateCopy.tableList[i].columns[j].constraint_type === 'UNIQUE') { + alterColumnsObj.rename_constraint = `unique_${schemaStateCopy.tableList[i].table_name}${schemaStateCopy.tableList[i].columns[j].column_name}`; + alterColumnsArray.push(alterColumnsObj); + } + } + const alterTablesObj: AlterTablesObjType = { + is_insertable_into: schemaStateCopy.tableList[i].is_insertable_into, + table_catalog: schemaStateCopy.tableList[i].table_catalog, + table_name: schemaStateCopy.tableList[i].table_name, + new_table_name: tableInputField.value, + table_schema: schemaStateCopy.tableList[i].table_schema, + addColumns: [], + dropColumns: [], + alterColumns: alterColumnsArray, + }; + + // update frontend + if (tableInputField !== null) { + schemaStateCopy.tableList[i].new_table_name = tableInputField.value; + setSchemaState(schemaStateCopy); + } + backendObj.current.updates.alterTables.push(alterTablesObj); + } + } + } + }; + + return ( +
+ + + + + Add Column + + + + + + +
+ ); +} + +export default TableHeader; diff --git a/frontend/components/views/ERTables/styles.css b/frontend/components/views/ERTables/styles.css new file mode 100644 index 00000000..658f6f48 --- /dev/null +++ b/frontend/components/views/ERTables/styles.css @@ -0,0 +1,62 @@ +.field-info-input { + display: flex; + justify-content: space-between; +} + +.field-info-input > input { + text-align: center; +} + +.field-info-dropdown { + display: flex; + justify-content: space-between; +} + +.field-info-checkbox { + display: flex; + justify-content: space-between; +} + +.checkbox-wrapper { + display: flex; + justify-content: space-around; + width: 50%; +} + +.field-summary-wrapper { + display: flex; + justify-content: space-between; + width: 100%; +} + +.field-summary-wrapper > p { + width: 50%; +} + +.field-summary-wrapper > p { + width: 50%; + text-align: left; +} + +#delete-btn { + float: right; +} + +.table-header > label { + color: '#57a777'; + font-size: 25px; + font-weight: bold; + margin: 2px; +} + +.table-header { + display: flex; + justify-content: center; + align-items: center; + background-color: white; + padding: 15px; + width: 350px; + margin: 0px; +} + + diff --git a/frontend/components/views/NewSchemaView/NewSchemaView.tsx b/frontend/components/views/NewSchemaView/NewSchemaView.tsx index 1bf20450..65cc2286 100644 --- a/frontend/components/views/NewSchemaView/NewSchemaView.tsx +++ b/frontend/components/views/NewSchemaView/NewSchemaView.tsx @@ -213,6 +213,7 @@ return ( tables={dbTables} selectTable={(table: TableInfo) => setSelectedTable(table)} selectedTable={selectedTable} + selectedDb={selectedDb} /> ); diff --git a/frontend/components/views/QueryView/ExecutionPlan/ExecutionPlanNodeTypes.tsx b/frontend/components/views/QueryView/ExecutionPlan/ExecutionPlanNodeTypes.tsx new file mode 100644 index 00000000..4f3d2a62 --- /dev/null +++ b/frontend/components/views/QueryView/ExecutionPlan/ExecutionPlanNodeTypes.tsx @@ -0,0 +1,7 @@ +import FlowNode from './FlowNodeComponent'; + +const nodeTypes = { + flowNode: FlowNode, +}; + +export default nodeTypes; diff --git a/frontend/components/views/QueryView/ExecutionPlan/FlowControls.tsx b/frontend/components/views/QueryView/ExecutionPlan/FlowControls.tsx index b5f8cf6b..0dc70c3b 100644 --- a/frontend/components/views/QueryView/ExecutionPlan/FlowControls.tsx +++ b/frontend/components/views/QueryView/ExecutionPlan/FlowControls.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { useZoomPanHelper } from 'react-flow-renderer'; +import { useReactFlow } from 'react-flow-renderer'; import { ButtonGroup, Button, Tooltip } from '@material-ui/core'; import FullscreenIcon from '@material-ui/icons/Fullscreen'; import FullscreenExitIcon from '@material-ui/icons/FullscreenExit'; @@ -36,7 +36,7 @@ const FlowControls = ({ setThresholds, }: FlowControlsProps) => { const [showThresholdsDialog, setShowThresholdsDialog] = useState(false); - const { fitView, zoomIn, zoomOut } = useZoomPanHelper(); + const { fitView, zoomIn, zoomOut } = useReactFlow(); const tooltipDelay = 1000; return ( diff --git a/frontend/components/views/QueryView/ExecutionPlan/FlowNodeComponent.tsx b/frontend/components/views/QueryView/ExecutionPlan/FlowNodeComponent.tsx new file mode 100644 index 00000000..6c8128a0 --- /dev/null +++ b/frontend/components/views/QueryView/ExecutionPlan/FlowNodeComponent.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Position, NodeProps, Handle } from 'react-flow-renderer'; +import PlanCard from './PlanCard'; +import { SizedPlanNode, Totals } from '../../../../lib/flow'; +import { Thresholds } from '../../../../types'; + +type FlowNodeProps = NodeProps<{ + plan: SizedPlanNode; + totals: Totals; + thresholds: Thresholds; +}>; + +const FlowNodeComponent = ({ + data: { plan, totals, thresholds }, +}: FlowNodeProps) => ( +
+ + + +
+); + +export default FlowNodeComponent; \ No newline at end of file diff --git a/frontend/components/views/QueryView/ExecutionPlan/PlanTree.tsx b/frontend/components/views/QueryView/ExecutionPlan/PlanTree.tsx index 7c2edc4b..eec3c210 100644 --- a/frontend/components/views/QueryView/ExecutionPlan/PlanTree.tsx +++ b/frontend/components/views/QueryView/ExecutionPlan/PlanTree.tsx @@ -1,61 +1,36 @@ import React, { useState, memo } from 'react'; import styled from 'styled-components'; -import ReactFlow, { - Background, - Handle, - Position, - NodeProps, -} from 'react-flow-renderer'; -import PlanCard from './PlanCard'; -import buildFlowGraph, { SizedPlanNode, Totals } from '../../../../lib/flow'; +import ReactFlow, { Background } from 'react-flow-renderer'; +import buildFlowGraph from '../../../../lib/flow'; import { ExplainJson, Thresholds } from '../../../../types'; import { DarkPaperFull } from '../../../../style-variables'; import FlowControls from './FlowControls'; - -type FlowNodeProps = NodeProps<{ - plan: SizedPlanNode; - totals: Totals; - thresholds: Thresholds; -}>; - -const FlowNodeComponent = ({ - data: { plan, totals, thresholds }, -}: FlowNodeProps) => ( -
- - - -
-); +import nodeTypes from './ExecutionPlanNodeTypes'; interface FlowTreeProps { data: ExplainJson; thresholds: Thresholds; } -const FlowTree = ({ data, thresholds }: FlowTreeProps) => ( - instance.fitView({ padding: 0.2 })} - // improves performance on pan by preventing contant rerenders at the - // cost of higher startup time - onlyRenderVisibleElements={false} - > - - -); +const FlowTree = ({ data, thresholds }: FlowTreeProps) => { + const result = buildFlowGraph(data,thresholds,'flowNode','smoothstep'); + return ( + instance.fitView({ padding: 0.2 })} + // improves performance on pan by preventing contant rerenders at the + // cost of higher startup time + onlyRenderVisibleElements={false} + > + + +)}; // Memoise to prevent rerender on fullscreen toggle const MemoFlowTree = memo(FlowTree); diff --git a/frontend/components/views/QueryView/TabSelector.tsx b/frontend/components/views/QueryView/TabSelector.tsx index ab136a22..c275dd70 100644 --- a/frontend/components/views/QueryView/TabSelector.tsx +++ b/frontend/components/views/QueryView/TabSelector.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { ButtonGroup, Button, Box } from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import FormatListBulletedIcon from '@material-ui/icons/FormatListBulleted'; -import { useZoomPanHelper } from 'react-flow-renderer'; +import {useReactFlow} from 'react-flow-renderer'; import styled from 'styled-components'; import { selectedColor, @@ -34,7 +34,7 @@ interface TabSelectorProps { } const TabSelector = ({ selectedTab, select }: TabSelectorProps) => { - const { fitView } = useZoomPanHelper(); + const { fitView } = useReactFlow(); const handleSelect = (tabName: ValidTabs) => () => { // fit view whenever execution plan view is selected diff --git a/frontend/components/views/QuickStartView.tsx b/frontend/components/views/QuickStartView.tsx index 76bc0047..90cb3e1c 100644 --- a/frontend/components/views/QuickStartView.tsx +++ b/frontend/components/views/QuickStartView.tsx @@ -30,10 +30,10 @@ const useStyles = makeStyles((theme: Theme) => marginBottom: theme.spacing(3), }, image: { - marginTop: theme.spacing(10), - marginBottom: theme.spacing(-2), - width: '30vh', - height: '30vh', + marginTop: theme.spacing(2), + marginBottom: theme.spacing(-4), + width: '20vh', + height: '20vh', maxHeight: '300px', maxWidth: '300px', }, @@ -49,7 +49,7 @@ const PageContainer = styled.a` align-items: center; justify-content: center; height: auto; - width: 100%; + width: auto; `; const StyledStepper = styled(Stepper)` @@ -58,14 +58,14 @@ const StyledStepper = styled(Stepper)` `; const StyledStepLabel = styled(StepLabel)` - width: 20vw; + width: 14vw; & .MuiStepLabel-label { - font-size: clamp(1rem, 2vw, 1.5rem); + font-size: clamp(1rem, 1.28vw, 1.5rem); } `; const StyledTypographyInstructions = styled.div` - font-size: clamp(1rem, 2vw, 1.3rem); + font-size: clamp(1rem, 2.2vw, 1.3rem); text-align: center; `; @@ -74,7 +74,7 @@ const StyledTypographyTitle = styled(Typography)` `; const NavButtons = styled.div` - margin: 20px auto; + margin: 15px auto; `; const StepContent = styled.div` @@ -89,7 +89,7 @@ const StepList = styled.ul` font-size: 0.9em; & li { - margin-top: 8px; + margin-top: 7px; } `; @@ -105,13 +105,13 @@ function getStepContent(step: number) { Step 1:
  • To import a database, select the + icon in the sidebar.
  • -
  • A modal will appear where you can enter a database name.
  • +
  • Enter a name for your database.
  • Click the green "Import File" button and select a .sql file.
  • -
  • The imported database will now appear on the sidebar.
  • -
  • You can select a database to view table information.
  • +
  • Select your imported database on the sidebar to view table information.
  • +
  • To view each table, click the name of the table in the top tabs bar. @@ -162,8 +162,8 @@ function getStepContent(step: number) { Step 3: -
  • To save a query, first you must declare a file location by clicking the "Designate Save Location" button in the queries tab
  • -
  • Then you can save your queries individually by clicking the "Save Query" button
  • +
  • To save a query, declare a file location by clicking the "Designate Save Location" button in the queries tab
  • +
  • Then, save queries individually by clicking the "Save Query" button
  • To load data into SeeQR just click the "Import Query" button, select the file you wish to upload in your local file system and click "Upload"
  • @@ -173,8 +173,8 @@ function getStepContent(step: number) { Step 4: -
  • To compare queries, select the checkbox of the queries inside the groups you would like to compare.
  • -
  • Then select the Chart Icon at the top of the sidebar.
  • +
  • Select the checkbox of the queries inside the groups you would like to compare.
  • +
  • Then, click the Chart Icon at the top of the sidebar.
  • Feel free to continually select and deselect queries to compare.
  • diff --git a/frontend/constants/constants.ts b/frontend/constants/constants.ts new file mode 100644 index 00000000..9c59f919 --- /dev/null +++ b/frontend/constants/constants.ts @@ -0,0 +1,7 @@ +/** + * This file contains the constants used throughout ER TABLING + */ + +export const TABLE_HEADER = 'tableHeader'; +export const TABLE_FIELD = 'tableField'; + diff --git a/frontend/lib/convertStateToReactFlow.js b/frontend/lib/convertStateToReactFlow.js new file mode 100644 index 00000000..0cd193a4 --- /dev/null +++ b/frontend/lib/convertStateToReactFlow.js @@ -0,0 +1,150 @@ +import fs from 'fs'; +import { remote } from 'electron'; +import { MarkerType } from 'react-flow-renderer'; +import { greenPrimary } from '../style-variables'; +import * as types from '../constants/constants'; + +/** + * This class creates a table instance which will get the data for + * the individual table and convert it to the form that react-flow is expecting + * for its nodes + */ +class Table { + constructor(id, columns, name, otherTables, database) { + this.id = id; + this.columns = columns; + this.name = name; + this.otherTables = otherTables; + this.database = database; + } + // the render method converts the data into the form of react flow + render() { + // This method gets the table table position from the stored file + const getTablePosition = () => { + const location = remote.app.getPath('temp').concat('/UserTableLayouts.json'); + try { + const data = fs.readFileSync(location, 'utf8'); + const parsedData = JSON.parse(data); + for (let i = 0; i < parsedData.length; i += 1) { + const db = parsedData[i]; + if (db.db_name === this.database) { + // eslint-disable-next-line consistent-return + for (let j = 0; j < db.db_tables.length; j += 1) { + const currTable = db.db_tables[j]; + if (currTable.table_name === this.name) + return currTable.table_position; + } + } + } + return { x: (this.id - 1) * 500, y: 0 }; + } catch (error) { + return { x: (this.id - 1) * 500, y: 0 }; + } + }; + // create a nodes array for react flow, the first element will always be a + // TABLE_HEADER type of node + const nodes = [ + { + id: `table-${this.name}`, + type: types.TABLE_HEADER, + position: getTablePosition(this.name, this.id), + tableName: this.name, + data: { + table_name: this.name, + }, + }, + ]; + + const edges = []; + // iterate through the columns data for this data, create a node for each column + // create an edge (the connection line) for each column that has a designated + // foreign table and foreign column name + this.columns.forEach((el, i) => { + // create a table field node for each column for react-flow + nodes.push({ + id: `table-${this.name}_column-${el.column_name}`, + type: types.TABLE_FIELD, + parentNode: `table-${this.name}`, + draggable: false, + position: { x: 0, y: (i + 1) * 78 }, + data: { + tableName: this.name, + columnData: el, + otherTables: this.otherTables, + }, + }); + + // if the element has a foregin_column and foreign_table create an edge + if (el.foreign_column && el.foreign_table) { + // create an edge for react flow + edges.push({ + source: `table-${this.name}_column-${el.column_name}`, + target: `table-${el.foreign_table}_column-${el.foreign_column}`, + id: `table-${this.name}_column${el.column_name}__table-${el.foreign_table}_column-${el.foreign_column}`, + markerEnd: { + type: MarkerType.Arrow, + color: greenPrimary, + }, + style: { strokeWidth: 6, stroke: greenPrimary }, + }); + } + }); + + // return an object with nodes and edges + return { + nodes, + edges, + }; + } +} + +const convertStateToReactFlow = { + convert: (schema) => { + // declare + const nodes = []; + const edges = []; + const tableList = []; + // iterate through the tableList + for (let i = 0; i < schema.tableList.length; i += 1) { + const column_names = []; + // get all the column names from the table + for (let j = 0; j < schema.tableList[i].columns.length; j += 1) { + column_names.push(schema.tableList[i].columns[j].column_name); + } + // push tablelsit an object with the table name and + tableList.push({ + table_name: schema.tableList[i].table_name, + column_names, + }); + } + for (let i = 0; i < schema.tableList.length; i += 1) { + // make a deep copy so that modifying these values will not affect the data + const copyString = JSON.stringify(schema.tableList[i]); + const copy = JSON.parse(copyString); + // filter the table that is the same from the tableList + const otherTableList = tableList.filter( + (el) => el.table_name !== schema.tableList[i].table_name + ); + // create a new instance of Table, push into table array + const table = new Table( + i + 1, + copy.columns, + schema.tableList[i].table_name, + otherTableList, + schema.database + ); + // assign the evaluated result of rendering the table into tablesNodesEdges + const tableNodesAndEdges = table.render(); + // each table will return an array of its nodes/edges + // spread the individual table nodes/edges and push to its corresponding array + nodes.push(...tableNodesAndEdges.nodes); + edges.push(...tableNodesAndEdges.edges); + } + return { + nodes, + edges, + }; + }, +}; + +export default convertStateToReactFlow; diff --git a/frontend/lib/flow.ts b/frontend/lib/flow.ts index 625cabee..bf423db7 100644 --- a/frontend/lib/flow.ts +++ b/frontend/lib/flow.ts @@ -1,12 +1,7 @@ -import { Elements } from 'react-flow-renderer'; import { PlanNode, ExplainJson, Thresholds } from '../types'; import { planNodeWidth, planNodeHeight } from '../style-variables'; import createLayout, { SizedNode, Graph } from './planLayout'; -export type TypedElements = Elements<{ - plan: PlanNode; -}>; - export type SizedPlanNode = PlanNode & SizedNode; export interface Totals { @@ -22,8 +17,8 @@ const dagreToFlow = ( thresholds: Thresholds, nodeType: string, edgeType: string -): TypedElements => { - const nodes: TypedElements = graphElements.nodes.map((node) => ({ +) => { + const nodes = graphElements.nodes.map((node) => ({ id: node.id, // NOTE: BREAKS IF CIRCULAR STRUCTURES ARE PASSED IN data: { plan: node.nodeData, totals, thresholds }, @@ -31,15 +26,17 @@ const dagreToFlow = ( type: nodeType, })); - const edges: TypedElements = graphElements.edges.map((edge) => ({ + const edges = graphElements.edges.map((edge) => ({ id: `${edge.nodes.from}-${edge.nodes.to}`, source: edge.nodes.from, target: edge.nodes.to, type: edgeType, animated: true, })); - - return nodes.concat(edges); + return { + nodes, + edges, + }; }; const traverse = ( @@ -93,14 +90,22 @@ const buildFlowGraph = ( thresholds: Thresholds, nodeComponent: string, edgeType: string -): TypedElements => { +) => { const sizedNodes = getSizedNodes(explain.Plan); // values to be injected into each plan const totals: Totals = { time: explain['Execution Time'] }; const layout = createLayout(sizedNodes); - return dagreToFlow(layout, totals, thresholds, nodeComponent, edgeType); + + const result = dagreToFlow( + layout, + totals, + thresholds, + nodeComponent, + edgeType + ); + return result; }; export default buildFlowGraph; diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts index 24a8b97f..bd49ad74 100644 --- a/frontend/lib/utils.ts +++ b/frontend/lib/utils.ts @@ -32,4 +32,5 @@ export const readingTime = (str: string) => { export const sendFeedback = (feedback: Feedback) => { const rendererId = window.require('electron').remote.getCurrentWebContents().id; ipcRenderer.sendTo(rendererId, 'feedback', feedback) -} \ No newline at end of file +} + diff --git a/frontend/types.ts b/frontend/types.ts index 2828a415..70519920 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -1,8 +1,15 @@ +import { TABLE_HEADER, TABLE_FIELD } from './constants/constants'; + /** * This file contains common types that need to be used across the frontend */ -type ViewName = 'compareView' | 'dbView' | 'queryView' | 'quickStartView' | 'newSchemaView'; +type ViewName = + | 'compareView' + | 'dbView' + | 'queryView' + | 'quickStartView' + | 'newSchemaView'; export interface AppState { selectedView: ViewName; @@ -18,12 +25,13 @@ export interface AppState { setSidebarHidden: (isHidden: boolean) => void; sidebarIsHidden: boolean; setFilePath: (filePath: string) => void; - newFilePath: string + newFilePath: string; + setERView?: (boolean) => void; } export interface FilePath { - cancelled: boolean, - filePath: string + cancelled: boolean; + filePath: string; } export type CreateNewQuery = (query: QueryData) => void; @@ -52,10 +60,9 @@ export interface QueryData { /** * User given group for acordian grouping */ - group: string + group: string; // flag: boolean - } export type ValidTabs = 'Results' | 'Execution Plan'; @@ -111,7 +118,7 @@ export interface TableColumn { /** * Can this column receive Null values or not? */ - is_nullable: 'yes' | 'no'; + is_nullable: 'YES' | 'NO'; } export interface TableInfo { @@ -139,7 +146,6 @@ export interface DbLists { tableList: TableInfo[]; } - /** * Type guard that checks if obj is compatible with type DbLists */ @@ -204,5 +210,128 @@ export interface ExplainJson { 'Execution Time': number; } +/** + * + * This section contains the types used for ER Tabling + * + * + */ + +export type ERTablingConstants = { TABLE_HEADER } | { TABLE_FIELD }; +export type NodeTypes = { + tableHeader: JSX.Element; + tableField: JSX.Element; +}; + +export type ERTableData = { + columns: ERTableColumnData[]; + is_insertable_into: 'yes' | 'no'; + table_catalog: string; + table_name: string; + table_schema: string; + new_table_name: string | null; +}; +export type ERTableColumnData = { + column_name: string; + new_column_name: string | null; + constraint_name: string | null; + constraint_type: string | null; + data_type: string; + character_maximum_length: number | null; + foreign_column: string; + foreign_table: string; + is_nullable: 'YES' | 'NO'; + unique?: boolean; // optional until implemented + auto_increment?: boolean; // optional until implemented +} +export type DataTypes = 'integer' | 'bigint' | 'varchar' | 'date' | null; + +export type AddColumnsObjType = { + column_name: string | null; + data_type: DataTypes; + character_maximum_length: number | null; +}; +export type DropColumnsObjType = { + column_name: string; +}; +export type AlterColumnsObjType = { + column_name: string; + character_maximum_length: number | null; + new_column_name: string | null; + add_constraint: AddConstraintObjType[]; + current_data_type: string | null; + data_type: string | null; + is_nullable: 'YES' | 'NO' | null; + drop_constraint: string[]; + rename_constraint: string | null; + table_schema: string | null; + table_name: string | null; + constraint_type: 'PRIMARY KEY' | 'FOREIGN KEY' | 'UNIQUE' | null; +}; + +export type AddConstraintObjType = { + constraint_type: 'PRIMARY KEY' | 'FOREIGN KEY' | 'UNIQUE' | null; + constraint_name: string; + foreign_table: string | null; + foreign_column: string | null; +}; + +export type AddTablesObjType = { + is_insertable_into: 'yes' | 'no'; + table_catalog: string; + table_name: string; + table_schema: string; + columns: ERTableColumnData[]; +}; +export type DropTablesObjType = { + table_name: string; + table_schema: string; +}; +export type AlterTablesObjType = { + is_insertable_into: 'yes' | 'no' | null; + table_catalog: string | null; + table_name: string; + new_table_name: string | null; + table_schema: string | null; + addColumns: AddColumnsObjType[]; + dropColumns: DropColumnsObjType[]; + alterColumns: AlterColumnsObjType[]; +}; + +export type UpdatesObjType = { + addTables: AddTablesObjType[]; + dropTables: DropTablesObjType[]; + alterTables: AlterTablesObjType[]; +}; + +export type BackendObjType = { + current: { + database: string; + updates: UpdatesObjType; + }; +}; + +export type SchemaStateObjType = { + database: string; + tableList: ERTableData[]; +}; + +export type TableHeaderDataObjectType = { + table_name: string; + schemaStateCopy: any; + setSchemaState: (string) => {}; + backendObj: BackendObjType; +}; + +export type TableHeaderNodeType = { + data: TableHeaderDataObjectType; + id: string; + position: { + x: number; + y: number; + }; + tableName: string; + type: string; +}; diff --git a/package.json b/package.json index 035d8c96..5069b5d8 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "seeqr", "productName": "SeeQR App", - "version": "6.0.1", - "description": "SQL toolbox - Execution Plan visualizer and Comparison tool", + "version": "7.0.0", + "description": "SQL Toolbox - Model Implementer, Execution Plan Visualizer and Query Comparison Tool", "main": "./tsCompiled/backend/main.js", "babel": { "presets": [ @@ -46,7 +46,6 @@ "cross-env": "^7.0.3", "dagre": "^0.8.5", "debounce": "^1.2.0", - "electron-devtools-installer": "^3.1.1", "faker": "^5.1.0", "fix-path": "^3.0.0", "fontsource-roboto": "^4.0.0", @@ -59,7 +58,7 @@ "react-chartjs-2": "^2.11.1", "react-codemirror": "^1.0.0", "react-dom": "^17.0.1", - "react-flow-renderer": "^9.0.1", + "react-flow-renderer": "^10.0.8", "react-loading": "^2.0.3", "react-router-dom": "^5.2.0", "sql-formatter": "^4.0.2",