diff --git a/.eslintignore b/.eslintignore index f49e5196..e4fadf68 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ tsCompiled assets dist node_modules -frontendOld \ No newline at end of file +frontendOld +jest.config.ts \ No newline at end of file diff --git a/DEV_README.md b/DEV_README.md index 60fcb146..361851a0 100644 --- a/DEV_README.md +++ b/DEV_README.md @@ -6,7 +6,7 @@ `Developer's Read Me` -`** v12.0.0 **` +`** v13.0.0 **`

In this version our team focused on refactoring the broken code base from all previous versions.

@@ -16,15 +16,20 @@ Run npm run dev twice if you do not manually run tsc to compile the files first.

WHAT WE UPDATED:

-

1. trimmed dependency issues from 54 down to 1. this one cannot be resolved because it is from the 30 viewers

-

2. Broke backend into MVVM/MVC model

-

3. Made sure types are enforced in typescript

-

4. Fixed import and export local files

-

5. Fixed Authentication

+

1. Refactored UX/UI to enhance user's performance by streamlining key features

+

2. Implemented Query History and selected query will populate on Monaco Editor

+

3. Changed from CodeMirror to Monaco Editor to bring back in colors for selected keywords

+

4. Reimplemented table rows to display in Table View

+

5. Fixed import and export local files for Postgres and MySQL

+

6. Fixed Authentication

+

7. Added additional testing suites

+

8. Introduced layout saving for ERD view

WHAT NEEDS TO BE DONE:

-

1. Isolating Database
One of the biggest tasks that we tried but did not finish is isolating the concerns of each database type (DBType). The current application has multiple

+

1. Refractor tableTabBar

+ +

2. Isolating Database
One of the biggest tasks that we tried but did not finish is isolating the concerns of each database type (DBType). The current application has multiple

if (database === DBType.postgres) {}
else if (database === DBType.mysql) {}
else (database === DBType.sqlite) {}
@@ -34,15 +39,15 @@ else (database === DBType.sqlite) {}
-

The road map is finish connecting the siloed pieces for postgres, then moving on to mysql

***Important***
There is not an entry for this system yet but this file frontend/components/iews/ERTables/ERDisplayWindow.tsx will replace frontend/components/iews/ERTables/ERTabling.tsx when this is ready

+

The road map is finish connecting the siloed pieces for postgres, then moving on to mysql

***Important***
There is not an entry for this system yet but this file frontend/components/iews/ERTables/ERDisplayWindow.tsx will be the entry

-

2. ERD Logic Update
Currently, previous wrote the frontend to send back a big bundle of all the operations done in the frontend ERD Table. This ERD table object is divided by add, drop, and alter. All the add operations will execute first then drop, then alter. This is BAD.

We need to redesign frontend to send back "sequental" operations instead of bundling operations by add, drop, alter because it takes care of multiple edge cases and users in the front can do as many operations they want to ensure SAVE works. I illustrated the problem below. The current backend is written out already. We just need to make sure the frontend is send back the appropriate logic

+

3. ERD Logic Update
Currently, previous wrote the frontend to send back a big bundle of all the operations done in the frontend ERD Table. This ERD table object is divided by add, drop, and alter. All the add operations will execute first then drop, then alter. This is BAD.

We need to redesign frontend to send back "sequental" operations instead of bundling operations by add, drop, alter because it takes care of multiple edge cases and users in the front can do as many operations they want to ensure SAVE works. I illustrated the problem below. The current backend is written out already. We just need to make sure the frontend is send back the appropriate logic



**_Important_**
This is wrtten at backend/src/ipcHandlers/dbCRUDHandlerERD.ts and will replace backend/src/ipcHandlers/dbCRUDHandler.ts when this is ready
-

3. Async event emmiters between front/backend

+

4. Async event emmiters between front/backend

Currently, the way the feedback modal works is by handling events that are emitted from both the frontend and the backend. Ideally, this should be refactored to be state dependent rather than event dependent, as it controls the display of the modal. This can be tied into the centralized async event emitter added to frontend/components/app.tsx, in conjunction with migration to reducers from state variables. The goal will be to house modal messages in the store tied to the main app reducer. From there, the async handler can send new messages to the state via main app dispatch, and any other front end feedback can do the same.

The main roadblock in the way of finalizing the transfer of event handlers out of the frontend is the way the dblist (list of databases in the sidebar) gets updated. Many event handlers in the backend send a dblist update event out to update the front end. Ideally, this should be handled by returning the new dblist changes out of the handler and using that resolved value to update state whenever an action would cause a dblist change. Right now, app.tsx has a useEffect running that listens for those dblist updates every frame. This is inefficient as a frontend concern.

The spinner currently works in a similar way to feedback. Once all async is completely migrated (including dblist update changes), this spinner can simply be tied to the loading property in the main app state.

@@ -52,3 +57,21 @@ There are still some filesystem read/write calls in the front end. This should b

5.

WHAT IS BROKEN:

+ +

1. The application on Windows may periodically crash.

+

2. There are import issues on mac computers.

+ +- Unable to import pg or mySQL database files +

3. Duplicates appear on previous queries.

+ +- In 'queryView', the 'queriesRan' state is defined, set, and passed down as a prop to its child component 'queryHistory'. On line 54 of 'queryHistory', duplicate query saved in the queriesRan state are removed. However, there's a problem: when we click the format button in QuerySqlInput and then run the query, it saves the query again. This happens because the new Set method doesn't recognize the formatted SQL strings due to the presence of '\n' characters. Consequently, clicking the run query button for both unformatted and formatted SQL strings results in duplicates being saved in the query history. + +

4. Label and Group field disappears.

+ +- In the queries tab, the Label and Group text in input field will disappear when selecting the Monaco Editor. +- The bug may arise from the useEffect hook, which triggers every time the component updates. Any changes detected in the editor results in a new Monaco Editor instance, potentially resetting the label and group text inputs. + +

5. Foreign and Primary keys.

+ +- Unable to select the primary and/or foreign key of a newly added column until the column is saved onto the database. Once saved onto the database, we can then select the primary and foreign key and save them onto the database. + diff --git a/README.md b/README.md index 003e0cb6..36f936e4 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/oslabs-beta/SeeQR) -![Release: 12.0.0](https://img.shields.io/badge/Release-12.0.0-red) +![Release: 13.0.0](https://img.shields.io/badge/Release-13.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) @@ -15,7 +15,7 @@ -### For the latest in-depth docs for v12.0.0, please visit our [docs site](http://www.seeqrapp.com/docs). +### For the latest in-depth docs for v13.0.0, please visit our [docs site](http://www.seeqrapp.com/docs). ## Table of Contents @@ -49,7 +49,7 @@ To get started on contributing and editing databases to this project: - [React-Flow](https://reactflow.dev/) - [Chart.js](https://github.com/chartjs) - [Faker.js](https://github.com/Marak/faker.js) -- [CodeMirror](https://codemirror.net/) +- [Monaco](https://microsoft.github.io/monaco-editor/) - [React-Force-Graph](https://github.com/vasturiano/react-force-graph) - [Three.js](https://github.com/mrdoob/three.js/) - [D3.js](https://github.com/d3/d3) @@ -76,6 +76,8 @@ To get started on contributing and editing databases to this project: - Databases - In the 'DATABASES' view, an interactive Entity Relationship Diagram (`ER DIAGRAM`) is displayed for the selected database. + - Users can now save table layout in version 13. + - 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. @@ -96,7 +98,7 @@ To get started on contributing and editing databases to this project:
-- NEW 3D Database Visualization +- 3D Database Visualization - From the 'DATABASES' view, select the '3D View' tab on the far right of the sidebar to launch the brand new 3D database visualizer. - Individual table nodes are distinguished by their different colors - Column nodes are joined to table nodes and match their source table color @@ -132,20 +134,19 @@ To get started on contributing and editing databases to this project: - Queries - - In the 'QUERIES' view, the main panel is where the query input text field is located, utilizing CodeMirror. The paint button in the top right corner of the panel auto-formats the inputted query. + - In the 'QUERIES' view, the main panel is where the query input text field is located, utilizing Monaco. The paint button in the top right corner of the panel auto-formats the inputted query. - Users can select the database to use in the 'Database' dropdown above the main panel. - Users also have the option to execute a labelled/grouped or unlabelled/ungrouped query — simply provide a label/group in the 'Label'/'Group' field above the main panel to identify the query in later comparisons against other queries. - Please note that only labelled queries will be saved in the current session for future references. - To execute the query, simply select the 'RUN QUERY' button at the bottom of the panel or press 'Ctrl-Enter' on the keyboard. - - The 11th version now includes the functionality to run multiple queries, allowing users to obtain more reliable testing results. -
+ - Users have the option to run multiple queries, allowing users to obtain more reliable testing results. + - Version 13 introduces a new feature that enables users to access and view previous queries. Upon selecting a previous query, it populates the query input field, allowing users to make edits before executing. +
+
- +

-
- -
- Save/Load Queries @@ -202,7 +203,7 @@ We've released SeeQR because it's a useful tool to help optimize SQL databases. ## Core Team -[Annabelle Ni](https://github.com/annni11) | [Derek Koh](https://github.com/derekoko) | [Peter Zepf](https://github.com/peterzepf) | [Tony Gao](https://github.com/tgao17) | [Ching-Yuan Lai (Eric)](https://github.com/paranoidFrappe) | [Jamie Zhang](https://github.com/haemie) | [Julian Macalalag](https://github.com/juzi3) | [Nathan Chong](https://github.com/nathanhchong) | [Junaid Ahmed](https://github.com/junaid-ahmed7) | [Chase Sizemore](https://github.com/ChaseSizemore) | [Oscar Romero](https://github.com/creaturenex) | [Anthony Deng](https://github.com/anthonyadeng) | [Aya Moosa](https://github.com/Hiya-its-Aya) | [Trevor Ferguson](https://github.com/TrevorJFerguson) | [Pauline Nguyen](https://github.com/paulinekpn) | [Utkarsh Uppal](https://github.com/utyvert) | [Fred Jeong](https://github.com/fred-jeong) | [Gabriel Kime](https://github.com/wizardbusiness) | [Chris Fryer](github.com/frynoceros) | [Ian Grepo](https://github.com/RadiantGH) | [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) +[Kevin Chou](https://github.com/choukevin612) |[Zoren Labrador](https://github.com/zorenal) |[Elaine Wong](https://github.com/user-byte123) | [Cathy Luong](https://github.com/cyliang93) | [Derek Koh](https://github.com/derekoko) | [Peter Zepf](https://github.com/peterzepf) | [Tony Gao](https://github.com/tgao17) | [Ching-Yuan Lai (Eric)](https://github.com/paranoidFrappe) | [Jamie Zhang](https://github.com/haemie) | [Julian Macalalag](https://github.com/juzi3) | [Nathan Chong](https://github.com/nathanhchong) | [Junaid Ahmed](https://github.com/junaid-ahmed7) | [Chase Sizemore](https://github.com/ChaseSizemore) | [Oscar Romero](https://github.com/creaturenex) | [Anthony Deng](https://github.com/anthonyadeng) | [Aya Moosa](https://github.com/Hiya-its-Aya) | [Trevor Ferguson](https://github.com/TrevorJFerguson) | [Pauline Nguyen](https://github.com/paulinekpn) | [Utkarsh Uppal](https://github.com/utyvert) | [Fred Jeong](https://github.com/fred-jeong) | [Gabriel Kime](https://github.com/wizardbusiness) | [Chris Fryer](github.com/frynoceros) | [Ian Grepo](https://github.com/RadiantGH) | [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/__tests__/backend/src/models/dbCRUDHAndler.spec.ts b/__tests__/backend/src/models/dbCRUDHAndler.spec.ts index dfe4484c..13a3c3c6 100644 --- a/__tests__/backend/src/models/dbCRUDHAndler.spec.ts +++ b/__tests__/backend/src/models/dbCRUDHAndler.spec.ts @@ -1,68 +1,275 @@ -describe('dbCRUDHandler tests', () => { - // setBaseConnections - describe('erTableSchemaUpdate tests', () => { - test('it should only receive backendObj as parameter', () => {}); - test('it should send async started back to frontend', () => {}); - test( - 'it should send backendObj to helper function to receive a queryString and a dbType back as query', - ); - test( - 'it should use query.queryString and query.dbType to run queryModel.query', +import { BackendObjType } from '../../../../shared/types/dbTypes'; +import { DBType, LogType, DBList } from '../../../../backend/BE_types'; +import { initializeDb, erTableSchemaUpdate } from '../../../../backend/src/ipcHandlers/handlers/dbCRUDHandler'; +import { Feedback } from '../../../../shared/types/utilTypes' +import queryModel from '../../../../backend/src/models/queryModel'; +import logger from '../../../../backend/src/utils/logging/masterlog'; +import helperFunctions from '../../../../backend/src/utils/helperFunctions'; +import connectionModel from '../../../../backend/src/models/connectionModel'; +import databaseModel from '../../../../backend/src/models/databaseModel'; + +import pools from '../../../../backend/src/db/poolVariables'; + +// local types +interface InitializePayload { + // handle initialization of a new schema from frontend (newSchemaView) + newDbName: string; + dbType: DBType; +} + +const { createDBFunc } = helperFunctions; + + + +// create a mock using jest.mock. For functions you are importing, set a key and the value with be a method jest.fn() +jest.mock('../../../../backend/src/ipcHandlers/handlers/dbCRUDHandler.ts', () => ({ + initializeDb: jest.fn(async (event, payload: InitializePayload) => { + const { newDbName, dbType } = payload; + logger( + `Received 'initialize-db' of dbType: ${dbType} and: `, + LogType.RECEIVE, + payload, ); - }); -}); - -describe('ertable-functions tests', () => { - describe('erdObjToQuery tests', () => { - test('it should only receive backendObj as parameter', () => {}); - test('it should create an empty array', () => {}); - test('it should identity the erdDbType from dbState', () => {}); - test('it should use erdDbType to pick an appropriate query function for backendObj to act on and receive a query bacj', () => {}); - test('it should return a query string'); - }); -}); - -/* -const backendObj = { + event.sender.send('async-started'); + + try { + // create new empty db + await queryModel.query(createDBFunc(newDbName, dbType), [], dbType); + // connect to initialized db + await connectionModel.connectToDB(newDbName, dbType); + + + // this causes a bottleneck. import DBList from BETypes + // update DBList in the sidebar to show this new db + + // const dbsAndTableInfo: DBList = await databaseModel.getLists( + // newDbName, + // dbType, + // ); + // event.sender.send('db-lists', dbsAndTableInfo); + // logger("Sent 'db-lists' from 'initialize-db'", LogType.SEND); + + } catch (e) { + const err = `Unsuccessful DB Creation for ${newDbName} in ${dbType} database`; + const feedback: Feedback = { + type: 'error', + message: err, + }; + event.sender.send('feedback', feedback); + } finally { + event.sender.send('async-complete'); + } + }), + + erTableSchemaUpdate: jest.fn(async (event, backendObj, dbName, dbType) => { + console.log(`Mocked erTableSchemaUpdate called with dbName: ${dbName}, dbType: ${dbType}`); + + // simulate sending notice to front end + event.sender.send('async-started'); + + // simulate a successful schema update + try { + // simulate generating query from backendObj + const query = 'mockQuery'; + // simulate running SQL commands + await queryModel.query('Begin;', [], dbType); + await queryModel.query(query, [], dbType); + await queryModel.query('Commit;', [], dbType); + + // simulate sending updated DB info to front end + const updatedDb = { }; + event.sender.send('db-lists', updatedDb); + + // simulate sending success feedback to front end + event.sender.send('feedback', { + type: 'success', + message: 'Database updated successfully.', + }); + + // simulate sending notice to front end that schema update has been completed + event.sender.send('async-complete'); + + // simulate logging + console.log("Sent 'db-lists and feedback' from 'erTableSchemaUpdate'"); + + // return a success message + return 'success'; + } catch (err) { + // simulate rolling back transaction on error + await queryModel.query('Rollback;', [], dbType); + + // return an error message + throw new Error('Mock error during schema update'); + } + }), +})); + + + +describe('dbCRUDHandler tests', () => { + + // mock event handler + const event = { sender: { send: jest.fn() } }; + + // simulate backendObj + const backendObj: BackendObjType = { database: 'tester2', updates: { - addTables: [ - { - is_insertable_into: 'yes', - table_name: 'NewTable8', - table_schema: 'puclic', - table_catalog: 'tester2', - columns: [] - } - ], - - dropTables: [{ - table_name: 'newtable5', - table_schema: 'puclic' + addTables: [ + { + is_insertable_into: 'yes', + table_name: 'NewTable8', + table_schema: 'public', + table_catalog: 'tester2', + columns: [], + }, + ], + + dropTables: [ + { + table_name: 'newtable5', + table_schema: 'public', + }, + ], + + alterTables: [ + { + is_insertable_into: null, + table_catalog: 'tester2', + table_name: 'newtable7', + new_table_name: null, + table_schema: 'public', + addColumns: [], + dropColumns: [], + alterColumns: [], + }, + { + is_insertable_into: null, + table_catalog: 'tester2', + table_name: 'newtable7', + new_table_name: null, + table_schema: 'public', + addColumns: [], + dropColumns: [], + alterColumns: [], + }, + ], + }, + }; + + + describe('initializeDb tests', () => { + // mock payload + const payloadpg = { newDbName: 'mockTest_pgdb', dbType: DBType.Postgres} + const payloadmsql = { newDbName: 'mockTest_msqldb', dbType: DBType.MySQL} + + test('it should receive an event and a payload containing newDbName and dbType', async () => { + await initializeDb(event, payloadpg); + expect(event.sender.send).toHaveBeenCalledWith('async-started'); + }) + + test('queryModel.query should be invoked with createDBFunc passing in payload and DBType for Postgres', async () => { + jest.spyOn(queryModel, "query") + await initializeDb(event, payloadpg); + expect(queryModel.query).toHaveBeenCalledWith(createDBFunc(payloadpg.newDbName, payloadpg.dbType), [], DBType.Postgres) + }); + + test('queryModel.query should be invoked with createDBFunc passing in payload and DBType for Postgres', async () => { + jest.spyOn(connectionModel, "connectToDB") + await initializeDb(event, payloadpg); + expect(connectionModel.connectToDB).toHaveBeenCalledWith(payloadpg.newDbName, payloadpg.dbType); + }); + + test('queryModel.query should be invoked with createDBFunc passing in payload and DBType for Postgres', async () => { + jest.spyOn(queryModel, "query") + await initializeDb(event, payloadmsql); + expect(queryModel.query).toHaveBeenCalledWith(createDBFunc(payloadmsql.newDbName, payloadmsql.dbType), [], DBType.MySQL) + }); + + test('should receive an error when db creation is unsuccessful', async () => { + try { + await initializeDb(event, payloadpg); + await queryModel.query(createDBFunc(payloadpg.newDbName, payloadpg.dbType), [], DBType.Postgres) + await connectionModel.connectToDB(payloadpg.newDbName, payloadpg.dbType) + } catch (e) { + const err = `Unsuccessful DB Creation for ${payloadpg.newDbName} in ${payloadpg.newDbName} database`; + const feedback: Feedback = { + type: 'error', + message: err, + }; + expect(e).toBe(event.sender.send('feedback', feedback)); } - ], - - alterTables: [ - { - is_insertable_into: null, - table_catalog: 'tester2', - table_name: 'newtable7', - new_table_name: null, - table_schema: 'puclic', - addColumns: [Array], - dropColumns: [], - alterColumns: [] - }, - { - is_insertable_into: null, - table_catalog: 'tester2', - table_name: 'newtable7', - new_table_name: null, - table_schema: 'puclic', - addColumns: [Array], - dropColumns: [], - alterColumns: [] - }] - } -} -*/ + }) + }) + + describe('erTableSchemaUpdate tests', () => { + test('it should receive an event, backendObj, dbtype, dbName as parameter', async () => { + const dbName: string = 'tester2'; + const dbType: DBType = DBType.Postgres; + await erTableSchemaUpdate(event, backendObj, dbName, dbType); + expect(event.sender.send).toHaveBeenCalledWith('async-started'); + }); + + + test('it should execute queryModel.query', async () => { + const dbName: string = 'tester2'; + const dbType: DBType = DBType.Postgres; + const query = 'mockQuery' + // checks for the result in the erTableSchemaUpdate + const actualResult = await erTableSchemaUpdate(event, backendObj, dbName, dbType); + // based on mock func, we are spying on queryModel - tracks when this method gets executed + const spyQuery = jest.spyOn(queryModel, "query"); + + // expect the spyQuery to have query, [], dbType + expect(spyQuery).toHaveBeenCalledWith(query, [], dbType); + // if the result is truthy then result should be success + expect(actualResult).toEqual('success'); + }); + + test('it should execute queryModel.query Begin', async () => { + const dbName: string = 'tester2'; + const dbType: DBType = DBType.Postgres; + // checks for the result in the erTableSchemaUpdate + const actualResult = await erTableSchemaUpdate(event, backendObj, dbName, dbType); + // based on mock func, we are spying on queryModel - tracks when this method gets executed + const spyQuery = jest.spyOn(queryModel, "query"); + + // expect the spyQuery to have 'Begin;', [], dbType + expect(spyQuery).toHaveBeenCalledWith('Begin;', [], dbType); + // if the result is truthy then result should be success + expect(actualResult).toEqual('success'); + }); + + test('it should execute queryModel.query Commit;', async () => { + const dbName: string = 'tester2'; + const dbType: DBType = DBType.Postgres; + // checks for the result in the erTableSchemaUpdate + const actualResult = await erTableSchemaUpdate(event, backendObj, dbName, dbType); + // based on mock func, we are spying on queryModel - tracks when this method gets executed + const spyQuery = jest.spyOn(queryModel, "query"); + + // expect the spyQuery to have 'Commit;', [], dbType + expect(spyQuery).toHaveBeenCalledWith('Commit;', [], dbType); + // if the result is truthy then result should be success + expect(actualResult).toEqual('success'); + }); + + }); + + test('it should send backendObj to helper function to receive a queryString and a dbType back as query', () => { + // const sqlString = 'SELECT * FROM example_table;'; + const updatedDb = { }; + // sends message to the event sender with the event name db-list + event.sender.send('db-lists', updatedDb); + // sending a message to the event sender. + event.sender.send('feedback', { + type: 'success', + message: 'Database updated successfully.', + }); + + const feedbackType = 'success'; + const messageType = 'Database updated successfully.'; + expect(typeof feedbackType).toBe('string'); + expect(typeof messageType).toBe('string'); + }); + }); \ No newline at end of file diff --git a/__tests__/backend/src/models/ertable-functions.spec.ts b/__tests__/backend/src/models/ertable-functions.spec.ts new file mode 100644 index 00000000..7e8ab197 --- /dev/null +++ b/__tests__/backend/src/models/ertable-functions.spec.ts @@ -0,0 +1,417 @@ +import backendObjToQuery from "../../../../backend/src/utils/ertable-functions"; +import { BackendObjType, DBType } from '../../../../shared/types/dbTypes'; +import { + AddTablesObjType, + DropTablesObjType, + AlterTablesObjType, + AlterColumnsObjType, + AddConstraintObjType, +} from '../../../../frontend/types'; + + +describe('ertable-functions tests', () => { + + // mock backendObj + const backendObj: BackendObjType = { + database: 'tester2', + updates: { + addTables: [ + { + is_insertable_into: 'yes', + table_name: 'NewTable8', + table_schema: 'public', + table_catalog: 'tester2', + columns: [], + }, + ], + + dropTables: [ + { + table_name: 'newtable5', + table_schema: 'public', + }, + ], + + alterTables: [ + { + is_insertable_into: null, + table_catalog: 'tester2', + table_name: 'newtable7', + new_table_name: null, + table_schema: 'public', + addColumns: [], + dropColumns: [], + alterColumns: [], + }, + ], + }, + }; + + + describe('backendObjToQuery tests', () => { + test('it should create a query string for Postgres database', () => { + const dbType = DBType.Postgres; + const result = backendObjToQuery(backendObj, dbType); + expect(typeof result).toBe('string'); + }); + + test('it should create a query string for MySQL database', ()=>{ + const dbType = DBType.MySQL; + const result = backendObjToQuery(backendObj, dbType); + expect(typeof result).toBe('string'); + }) + + test('it should invoke addTable passing in an addtable and altertable arrays', () => { + const dbType = DBType.Postgres; + // need an outputArray, which was in the outer scope of addTable + const outputArray: string[] = []; + // mock function for addTable, copied and pasted from erTable-functions.ts + const addTable = jest.fn(( + addTableArray: AddTablesObjType[], + alterTablesArray: AlterTablesObjType[], + ): void => { + for (let i = 0; i < addTableArray.length; i += 1) { + const currTable: AddTablesObjType = addTableArray[i]; + const currAlterTable: AlterTablesObjType = alterTablesArray[i] + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) { + outputArray.push( + `CREATE TABLE ${currTable.table_schema}.${currTable.table_name}(); ` + ); + } + } + }) + + // invoke addTable passining in params + addTable(backendObj.updates.addTables, backendObj.updates.alterTables); + expect(addTable).toBeCalledWith(backendObj.updates.addTables, backendObj.updates.alterTables); + + // output array should have a string passed into output array. table name and schema are from the mock obj. + expect(outputArray).toEqual(["CREATE TABLE public.NewTable8(); "]); + }); + }); + + test('it should invoke dropTable passing in a dropTable array', () => { + const dbType = DBType.Postgres; + // need an outputArray, which was in the outer scope of addTable + const outputArray: string[] = []; + // mock function for dropTable, copied and pasted from erTable-functions.ts + const dropTable = jest.fn((dropTableArray: DropTablesObjType[]): void => { + for (let i = 0; i < dropTableArray.length; i += 1) { + const currTable: DropTablesObjType = dropTableArray[i]; + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) { + outputArray.push( + `DROP TABLE ${currTable.table_schema}.${currTable.table_name}; `, + ); + } + } + }) + + // invoke dropTable passining in params + dropTable(backendObj.updates.dropTables); + expect(dropTable).toBeCalledWith(backendObj.updates.dropTables); + + // output array should have a string passed into output array. table name and schema are from the mock obj. + expect(outputArray).toEqual(["DROP TABLE public.newtable5; "]); + }); + + + + test('it should invoke alterTable passing in an alterTable array', () => { + const dbType = DBType.Postgres; + const outputArray: string[] = []; + const alterTables = jest.fn((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) { + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + 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}); `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) { + // let lengthOfData = ''; + // if (currTable.addColumns[i].character_maximum_length != null) { + // lengthOfData = `(${currTable.addColumns[i].character_maximum_length})`; + // } + // if ( + // firstAddingMySQLColumnName === null || + // firstAddingMySQLColumnName !== + // `${currTable.addColumns[i].column_name}` + // ) { + // addColumnString += `ALTER TABLE ${currTable.table_name} ADD COLUMN ${currTable.addColumns[i].column_name} ${currTable.addColumns[i].data_type} ${lengthOfData}; `; + // } + // } + // if (dbType === DBType.SQLite) + // addColumnString += `ALTER TABLE ${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) { + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + dropColumnString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} DROP COLUMN ${currTable.dropColumns[i].column_name}; `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) + // dropColumnString += `ALTER TABLE ${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; + + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + 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}'); `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) + // alterTableConstraintString += `ALTER TABLE ${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 { + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + 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}); `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) + // alterTableConstraintString += `ALTER TABLE ${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 { + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + alterTableConstraintString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} ADD CONSTRAINT ${currConstraint.constraint_name} UNIQUE (${currColumn.column_name}); `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) + // alterTableConstraintString += `ALTER TABLE ${currTable.table_name} ADD CONSTRAINT ${currConstraint.constraint_name} UNIQUE (${currColumn.column_name}); `; + } + // Remove constraint from column + function dropConstraint(currDrop): void { + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + alterTableConstraintString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} DROP CONSTRAINT ${currDrop}; `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) + // alterTableConstraintString += `ALTER TABLE ${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 (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) { + 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 (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) { + 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}); `; + } + } + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) { + // if (currTable.alterColumns[i].character_maximum_length) { + // alterMaxCharacterLengthString += `ALTER TABLE ${currTable.table_name} MODIFY COLUMN ${currTable.alterColumns[i].column_name} ${currTable.alterColumns[i].data_type}(${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)}`, + ); + } + }) + + alterTables(backendObj.updates.alterTables); + expect(alterTables).toBeCalledWith(backendObj.updates.alterTables); + expect(outputArray).toEqual([""]); + expect(outputArray).not.toEqual(["ALTER TABLE public.newTable7 ALTER COLUMN newTable7.mockColumn TYPE varchar(255); "]) + }); + + + test('it should invoke renameTablesColumns passing in an alterTable array', () => { + + const outputArray: string[] = []; + const dbType = DBType.Postgres; + const renameTablesColumns = jest.fn((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 = {}; + const outputArray: string[] = []; + // 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 + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + renameString += `ALTER TABLE ${currColumn.table_schema}.${currColumn.table_name} RENAME COLUMN ${currColumn.column_name} TO ${currColumn.new_column_name}; `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) + // renameString += `ALTER TABLE ${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 + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + renameString += `ALTER TABLE ${currTable.table_schema}.${currTable.table_name} RENAME TO ${currTable.new_table_name}; `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) + // renameString += `ALTER TABLE ${currTable.table_name} RENAME ${currTable.new_table_name}; `; + } + // Constraint names might not be compatible with databases with other naming conventions and the query will fail + // 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]]; + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) + renameString += `ALTER TABLE ${currColumn.table_schema}.${currColumn.table_name} RENAME CONSTRAINT ${constraintsToRename[i]} TO ${currColumn.constraint_type}_${currColumn.table_name}${currColumn.column_name}; `; + // if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) + // renameString += `ALTER TABLE ${currColumn.table_name} RENAME CONSTRAINT ${constraintsToRename[i]} TO ${currColumn.constraint_type}_${currColumn.table_name}${currColumn.column_name}; `; + } + outputArray.push(renameString); + }) + + + renameTablesColumns(backendObj.updates.alterTables); + expect(renameTablesColumns).toBeCalledWith(backendObj.updates.alterTables); + + // alter column array is empty in mock backendObj + expect(outputArray).toEqual([]); + expect(outputArray).not.toEqual(["ALTER TABLE public.newTable7 RENAME COLUMN mockColumn TO mockColumn2; "]); + + }); +}); + diff --git a/__tests__/backend/src/utils/erdAlters/pSqlCUD.spec.ts b/__tests__/backend/src/utils/erdAlters/pSqlCUD.spec.ts index 31e87f21..1971ffa0 100644 --- a/__tests__/backend/src/utils/erdAlters/pSqlCUD.spec.ts +++ b/__tests__/backend/src/utils/erdAlters/pSqlCUD.spec.ts @@ -2,6 +2,7 @@ import { generatePostgresColumnQuery, queryPostgres, } from '../../../../../backend/src/utils/erdCUD/pSqlCUD'; + import { PsqlColumnOperations, ErdUpdatesType, diff --git a/__tests__/frontend/lib/TableView.test.tsx b/__tests__/frontend/lib/TableView.test.tsx new file mode 100644 index 00000000..404ed92e --- /dev/null +++ b/__tests__/frontend/lib/TableView.test.tsx @@ -0,0 +1,99 @@ +// global.TextEncoder = require('util').TextEncoder; + +// import React from 'react'; +// import { render, screen, cleanup } from '@testing-library/react'; +// import TablesTabs from '../../../frontend/components/views/DbView/TablesTabBar'; +// import { DBType } from '../../../backend/BE_types'; +// import { TableInfo, TableColumn, AppState } from '../../../frontend/types'; +// import '@testing-library/jest-dom'; + +// const setERViewMock = jest.fn(); +// const selectTableMock = jest.fn(); + +// test('', ()=> { + +// }) + +// test('testing rendering of TablesTabs', () => { +// let active = false; +// const tables = [ +// { +// table_catalog: 'test', +// table_schema: 'public', +// table_name: 'newtable1', +// is_insertable_into: 'yes', +// columns: [ +// { +// column_name: 'newcolumn1', +// data_type: 'character varying', +// character_maximum_length: 255, +// is_nullable: 'YES', +// // constraint_name: null, +// // constraint_type: null, +// // foreign_table: null, +// // foreign_column: null, +// }, +// { +// column_name: 'newcolumn2', +// data_type: 'character varying', +// character_maximum_length: 255, +// is_nullable: 'YES', +// // constraint_name: null, +// // constraint_type: null, +// // foreign_table: null, +// // foreign_column: null, +// }, +// ] as TableColumn[], +// }, +// { +// table_catalog: 'test', +// table_schema: 'public', +// table_name: 'newtable2', +// is_insertable_into: 'YES', +// columns: [ +// { +// column_name: 'newcolumn1', +// data_type: 'character varying', +// character_maximum_length: 255, +// is_nullable: 'YES', +// // constraint_name: null, +// // constraint_type: null, +// // foreign_table: null, +// // foreign_column: null, +// }, +// ], +// }, +// ] as TableInfo[]; + +// let selectedTable = { +// table_catalog: 'test', +// table_schema: 'public', +// table_name: 'newtable2', +// is_insertable_into: 'YES', +// columns: [ +// { +// column_name: 'newcolumn1', +// data_type: 'character varying', +// character_maximum_length: 255, +// is_nullable: 'YES', +// constraint_name: null, +// constraint_type: null, +// foreign_table: null, +// foreign_column: null, +// }, +// ], +// }; + +// render( +// , +// ); +// const tableName = screen.getByText('newtable1'); +// expect(tableName).toBeInTheDocument(); +// }); diff --git a/__tests__/frontend/lib/addDbModal.spec.ts b/__tests__/frontend/lib/addDbModal.spec.ts new file mode 100644 index 00000000..4550f722 --- /dev/null +++ b/__tests__/frontend/lib/addDbModal.spec.ts @@ -0,0 +1,54 @@ +import path from 'path'; +import * as fs from 'fs' + +describe('AddNewDbModal import modal', () => { + + describe('Find special keywords from import file', () => { + + it('should be a .sql file', () => { + const filePath = path.join(__dirname, '../../mockDBFiles/starwarspg.sql') + expect(filePath.endsWith('sql')).toBe(true); + }) + + it('should check if .sql file DOES contain keywords', () => { + // starwars does contain the keyword + const filePath = path.join(__dirname, '../../mockDBFiles/starwarspg.sql') + + // reads the mock database + const data = fs.readFileSync(filePath, 'utf-8').replace(/`([^`]+)`|\b([a-zA-Z_]+)\b/g, '$1$2').replace(/;/g, '').match(/\S+/g) || []; + + // iterate through data and check if it contains the following keywords + const containsKeywords = data.some(word => ['CREATE', 'DATABASE'].includes(word.toUpperCase())); + + if(containsKeywords) { + // iternate thorugh data to check if create & database is next to each other + for(let i = 0; i < data.length; i+= 1) { + // if truthy, database file does contain the keywords + if(data[i] === 'CREATE' && data[i + 1] === 'DATABASE') { + expect(containsKeywords).toBe(true); + } + } + } + }) + + it('should check if .sql file DOES NOT contain keywords', ()=>{ + // dellstore does not contain the keyword + const filePath = path.join(__dirname, '../../mockDBFiles/dellstorepg.sql') + + const data = fs.readFileSync(filePath, 'utf-8').replace(/`([^`]+)`|\b([a-zA-Z_]+)\b/g, '$1$2').replace(/;/g, '').match(/\S+/g) || []; + // iterate through data and check if it contains the following keywords + const containsKeywords = data.some(word => ['CREATE', 'DATABASE', 'USE'].includes(word.toUpperCase())); + + if(containsKeywords) { + // iternate thorugh data to check if create & database is next to each other + for(let i = 0; i < data.length; i+= 1) { + // if falsey, database file does not contain the keywords + if(data[i] === 'CREATE' && data[i + 1] === 'DATABASE') { + expect(containsKeywords).toBe(false); + } + } + } + }) + }) +}) + diff --git a/__tests__/frontend/lib/dummyDataMain.spec.ts b/__tests__/frontend/lib/dummyDataMain.spec.ts new file mode 100644 index 00000000..2bf52746 --- /dev/null +++ b/__tests__/frontend/lib/dummyDataMain.spec.ts @@ -0,0 +1,10 @@ +import { getRandomInt } from '../../../backend/src/utils/dummyData/dummyDataMain'; + +describe('dummyData generated', () => { + it('should return a integer that lands between -Inf and Inf', () => { + const result = getRandomInt(-100, 100); + expect(typeof getRandomInt(-100, 100) === 'number').toEqual(true); + expect(result >= -100).toBeTruthy(); + expect(result < 100).toBeTruthy(); + }); +}); diff --git a/__tests__/frontend/lib/unittesting.spec.ts b/__tests__/frontend/lib/unittesting.spec.ts new file mode 100644 index 00000000..95c99a80 --- /dev/null +++ b/__tests__/frontend/lib/unittesting.spec.ts @@ -0,0 +1,56 @@ +global.TextEncoder = require('util').TextEncoder; + +// jest.mock('../../../backend/src/models/connectionModel'); +// jest.mock('../../../backend/src/models/queryModel'); + +import { runSelectAllQuery } from '../../../backend/src/ipcHandlers/handlers/queryHandler'; +import connectionModel from '../../../backend/src/models/connectionModel'; +import queryModel from '../../../backend/src/models/queryModel'; + +window.require = ((str: string) => str) as any; +// const mockConnectionModel = { +// connectToDB: jest.fn().mockResolvedValue('pg'), +// }; + +// const mockQueryModel = { +// query: jest.fn().mockResolvedValue([{ newcolumn1: 'hi' }]), +// }; + +jest.mock('../../../backend/src/models/connectionModel', () => ({ + __esModule: true, + default: { + connectToDB: jest.fn().mockResolvedValue('pg'), // Mock the connectToDB function + }, +})); + +jest.mock('../../../backend/src/models/queryModel', () => ({ + __esModule: true, + default: { + query: jest.fn().mockResolvedValue({ rows: [{ newcolumn1: 'hi' }] }), // Mock the query function directly + }, +})); + +// Typecast the modules to their mock counterparts +const mockConnectionModel = connectionModel as jest.Mocked< + typeof connectionModel +>; +const mockQueryModel = queryModel as jest.Mocked; +describe('runSelectAllQuery', () => { + it('should run select all query and return results', async () => { + const event = {}; + const sqlString = 'SELECT * FROM newtable1'; + const selectedDb = 'test'; + const curDBType = 'pg'; + const result = await runSelectAllQuery( + event, + { sqlString, selectedDb }, + curDBType, + ); + console.log(result); + + expect(mockConnectionModel.connectToDB).toHaveBeenCalledWith('test', 'pg'); + expect(mockConnectionModel.connectToDB).toHaveBeenCalledTimes(1); + expect(mockQueryModel.query).toHaveBeenCalledTimes(1); + expect(result).toEqual([{ newcolumn1: 'hi' }]); + }); +}); diff --git a/assets/readmeImages/gifs/query.gif b/assets/readmeImages/gifs/query.gif new file mode 100644 index 00000000..669c9ec5 Binary files /dev/null and b/assets/readmeImages/gifs/query.gif differ diff --git a/backend/BE_types.ts b/backend/BE_types.ts index 3832a34f..b6cfebd7 100644 --- a/backend/BE_types.ts +++ b/backend/BE_types.ts @@ -131,3 +131,8 @@ export interface QueryPayload { selectedDb: string; runQueryNumber: number; } + +export interface SelectAllQueryPayload { + sqlString: string; + selectedDb: string; +} diff --git a/backend/src/ipcHandlers/handlers/dbCRUDHandler.ts b/backend/src/ipcHandlers/handlers/dbCRUDHandler.ts index 318299dd..627c002d 100644 --- a/backend/src/ipcHandlers/handlers/dbCRUDHandler.ts +++ b/backend/src/ipcHandlers/handlers/dbCRUDHandler.ts @@ -43,7 +43,7 @@ interface UpdatePayload { * 4. send a feedback back to frontend */ -export async function intializeDb(event, payload: InitializePayload) { +export async function initializeDb(event, payload: InitializePayload) { const { newDbName, dbType } = payload; logger( `Received 'initialize-db' of dbType: ${dbType} and: `, diff --git a/backend/src/ipcHandlers/handlers/dbOpsHandler.ts b/backend/src/ipcHandlers/handlers/dbOpsHandler.ts index 26e7d810..adefaa71 100644 --- a/backend/src/ipcHandlers/handlers/dbOpsHandler.ts +++ b/backend/src/ipcHandlers/handlers/dbOpsHandler.ts @@ -228,7 +228,7 @@ export async function duplicateDb( ? runFullCopyFunc(sourceDb, tempFilePath, dbType) : runHollowCopyFunc(sourceDb, tempFilePath, dbType); try { - await promExecute(dumpCmd); + await promExecute(dumpCmd, dbType); } catch (e) { throw new Error( `Failed to dump ${sourceDb} to temp file at ${tempFilePath}`, @@ -243,16 +243,16 @@ export async function duplicateDb( } // run temp sql file on new database - try { - await promExecute(runSQLFunc(newName, tempFilePath, dbType)); - } catch (e: any) { - // cleanup: drop created db - logger(`Dropping duplicate db because: ${e.message}`, LogType.WARNING); - const dropDBScript = dropDBFunc(newName, dbType); - await queryModel.query(dropDBScript, [], dbType); - - throw new Error('Failed to populate newly created database'); - } + // try { + // await promExecute(runSQLFunc(newName, tempFilePath, dbType)); + // } catch (e: any) { + // // cleanup: drop created db + // logger(`Dropping duplicate db because: ${e.message}`, LogType.WARNING); + // const dropDBScript = dropDBFunc(newName, dbType); + // await queryModel.query(dropDBScript, [], dbType); + + // throw new Error('Failed to populate newly created database'); + // } // update frontend with new db list const dbsAndTableInfo: DBList = await databaseModel.getLists('', dbType); @@ -293,21 +293,30 @@ export async function importDb( event.sender.send('async-started'); try { - // create new empty database + + // if (dbType === DBType.Postgres) { + // try { + // await queryModel.query(createDBFunc(newDbName, dbType), [], dbType); + // } catch (e) { + // throw new Error('Failed to create Database'); + // } + // } + + // create new empty database try { await queryModel.query(createDBFunc(newDbName, dbType), [], dbType); } catch (e) { throw new Error('Failed to create Database'); } - + // run temp sql file on new database try { - await promExecute(runSQLFunc(newDbName, filePath, dbType)); + await promExecute(runSQLFunc(newDbName, filePath, dbType), dbType); } catch (e: any) { // cleanup: drop created db logger(`Dropping duplicate db because: ${e.message}`, LogType.WARNING); - const dropDBScript = dropDBFunc(newDbName, dbType); - await queryModel.query(dropDBScript, [], dbType); + + await queryModel.query(dropDBFunc(newDbName, dbType), [], dbType); throw new Error('Failed to populate newly created database'); } @@ -348,7 +357,7 @@ export async function exportDb(event, payload: ExportPayload, dbType: DBType) { // dump database to file const dumpCmd = runFullCopyFunc(db, filePath, dbType); try { - await promExecute(dumpCmd); + await promExecute(dumpCmd, dbType); } catch (e) { throw new Error(`Failed to dump ${db} to temp file at ${filePath}`); } diff --git a/backend/src/ipcHandlers/handlers/queryHandler.ts b/backend/src/ipcHandlers/handlers/queryHandler.ts index 7a7d33b2..0e67b601 100644 --- a/backend/src/ipcHandlers/handlers/queryHandler.ts +++ b/backend/src/ipcHandlers/handlers/queryHandler.ts @@ -1,7 +1,7 @@ import fs from 'fs'; // Types -import { DBList, DBType, LogType, QueryPayload } from '../../../BE_types'; +import { DBList, DBType, LogType, QueryPayload, SelectAllQueryPayload } from '../../../BE_types'; // Helpers import logger from '../../utils/logging/masterlog'; @@ -76,7 +76,7 @@ export async function runQuery( // Run Explain let explainResults; try { - for (let i = 0; i < numberOfSample; i++) { + // for (let i = 0; i < numberOfSample; i++) { if (dbType === DBType.Postgres) { const results = await queryModel.query( explainQuery(sqlString, dbType), @@ -102,6 +102,7 @@ export async function runQuery( arr.push(eachSampleTime); totalSampleTime += eachSampleTime; + // hard coded explainResults just to get it working for now explainResults = { Plan: { @@ -198,7 +199,7 @@ export async function runQuery( 'Execution Time': 9999, }; } - } + // } // get 5 decimal points for sample time minimumSampleTime = Math.round(Math.min(...arr) * 10 ** 5) / 10 ** 5; maximumSampleTime = Math.round(Math.max(...arr) * 10 ** 5) / 10 ** 5; @@ -257,7 +258,51 @@ export async function runQuery( event.sender.send('async-complete'); } } + +export async function runSelectAllQuery(event, {sqlString, selectedDb}:SelectAllQueryPayload, curDBType) { + // if (selectedDb !== targetDb) + try { + await connectionModel.connectToDB(selectedDb, curDBType); + const results = await queryModel.query(sqlString, [], curDBType); + console.log('good',results.rows) + return results?.rows + } catch (error) { + console.log(error, 'in runSelectAllQuery') + } +} +//format of runQuery without all extra junk +// export async function runQuery( +// event, +// { targetDb, sqlString, selectedDb, runQueryNumber }: QueryPayload, +// dbType: DBType, +// ) { +// try{ +// await connectionModel.connectToDB(targetDB, dbType); +// let returnedRows; +// try { +// const results = await queryModel.query(sqlString, [], dbType); +// if (dbType === DBType.MySQL) { +// returnedRows = results[0]; +// } +// if (dbType === DBType.Postgres) { +// returnedRows = results?.rows; +// } +// if (dbType === DBType.SQLite) { +// returnedRows = results; +// } +// } catch (e: any) { +// error = e.toString(); +// } + +// return { +// returnedRows +// } +// } + +//finally { +// } +// } // Reads the query JSON file and send it to the front end export function readQuery(event, filepath) { try { diff --git a/backend/src/ipcHandlers/index.ts b/backend/src/ipcHandlers/index.ts index 203ff0be..e27cebc6 100644 --- a/backend/src/ipcHandlers/index.ts +++ b/backend/src/ipcHandlers/index.ts @@ -4,7 +4,7 @@ import { ipcMain } from 'electron'; import { setConfig, getConfig } from './handlers/authHandler'; import { - intializeDb, + initializeDb, updateDb, erTableSchemaUpdate, getPath, @@ -17,7 +17,7 @@ import { importDb, exportDb, } from './handlers/dbOpsHandler'; -import { runQuery, readQuery } from './handlers/queryHandler'; +import { runQuery, readQuery, runSelectAllQuery } from './handlers/queryHandler'; import { dummyData, showOpenDialog, @@ -39,7 +39,7 @@ ipcMain.handle('export-db', exportDb); ipcMain.handle('get-path', getPath); // db CRUD functionalities -ipcMain.handle('initialize-db', intializeDb); +ipcMain.handle('initialize-db', initializeDb); ipcMain.handle('update-db', updateDb); ipcMain.handle('ertable-schemaupdate', erTableSchemaUpdate); @@ -52,3 +52,6 @@ ipcMain.handle('generate-dummy-data', dummyData); ipcMain.handle('showOpenDialog', showOpenDialog); ipcMain.handle('showSaveDialog', showSaveDialog); ipcMain.handle('feedback', feedback); + + +ipcMain.handle('run-select-all-query', runSelectAllQuery); diff --git a/backend/src/models/configModel.ts b/backend/src/models/configModel.ts index 3a4b37ff..3fb5fd5f 100644 --- a/backend/src/models/configModel.ts +++ b/backend/src/models/configModel.ts @@ -1,13 +1,12 @@ // import path from 'path'; import fs from 'fs'; -import os from 'os'; import { DBType, LogType } from '../../../shared/types/dbTypes'; import logger from '../utils/logging/masterlog'; import { DocConfigFile } from '../../BE_types'; // HELPER FUNCTIONS -const home = `${os.homedir()}/Documents/SeeQR`; +const home = process.cwd(); const configFile = 'config.json'; const configPath = `${home}/${configFile}`; diff --git a/backend/src/utils/dummyData/dummyDataMain.ts b/backend/src/utils/dummyData/dummyDataMain.ts index a7085e70..476afbc2 100644 --- a/backend/src/utils/dummyData/dummyDataMain.ts +++ b/backend/src/utils/dummyData/dummyDataMain.ts @@ -16,7 +16,7 @@ import queryModel from '../../models/queryModel'; // helper function to generate random numbers that will ultimately represent a random date -const getRandomInt = (min: number, max: number) => { +export const getRandomInt = (min: number, max: number) => { const minInt = Math.ceil(min); const maxInt = Math.floor(max); // The maximum is exclusive and the minimum is inclusive @@ -26,6 +26,7 @@ const getRandomInt = (min: number, max: number) => { // helper function to generate random data based on a given column's data type const generateDataByType = (columnObj: ColumnObj): string | number => { let length; + console.log(columnObj.data_type); // updated the new faker package so updated to follow proper documentation. switch (columnObj.data_type) { case 'smallint': diff --git a/backend/src/utils/ertable-functions.ts b/backend/src/utils/ertable-functions.ts index 1bc2d1b3..4165f0b5 100644 --- a/backend/src/utils/ertable-functions.ts +++ b/backend/src/utils/ertable-functions.ts @@ -38,14 +38,23 @@ function backendObjToQuery(backendObj: BackendObjType, dbType: DBType): string { ); } if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) { - firstAddingMySQLColumnName = `${currAlterTable.addColumns[0].column_name}`; - outputArray.push( - `CREATE TABLE ${currTable.table_name} - (${currAlterTable.addColumns[0].column_name} - ${currAlterTable.addColumns[0].data_type} - (${currAlterTable.addColumns[0].character_maximum_length})) - ;`, - ); + + // check if addColumns is defined and not empty before accessing its properties + if ( + currAlterTable.addColumns && + currAlterTable.addColumns.length > 0 + ) { + firstAddingMySQLColumnName = `${currAlterTable.addColumns[0].column_name}`; + outputArray.push( + `CREATE TABLE ${currTable.table_name} + (${currAlterTable.addColumns[0].column_name} + ${currAlterTable.addColumns[0].data_type} + (${currAlterTable.addColumns[0].character_maximum_length})) + ;`, + ); + } else { + console.error("addColumns is undefined or empty for MySQL."); + } } if (dbType === DBType.SQLite) { diff --git a/backend/src/utils/helperFunctions.ts b/backend/src/utils/helperFunctions.ts index 740b101d..3722d37f 100644 --- a/backend/src/utils/helperFunctions.ts +++ b/backend/src/utils/helperFunctions.ts @@ -21,7 +21,7 @@ interface HelperFunctions { runTARFunc: CreateCommand; runFullCopyFunc: CreateCommand; runHollowCopyFunc: CreateCommand; - promExecute: (cmd: string) => Promise<{ stdout: string; stderr: string }>; + promExecute: (cmd: string, dbType: DBType) => Promise<{ stdout: string; stderr: string }>; } // PG = Postgres - Query necessary to run PG Query/Command @@ -73,18 +73,18 @@ const helperFunctions: HelperFunctions = { // import SQL file into new DB created runSQLFunc: function runSQLFunc(dbName, file, dbType: DBType) { const SQL_data = docConfig.getFullConfig(); - const PG = `PGPASSWORD=${SQL_data?.pg_options.password} psql -U ${SQL_data?.pg_options.user} -d "${dbName}" -f "${file}" -p ${SQL_data?.pg_options.port}`; - const MYSQL = `export MYSQL_PWD='${SQL_data?.mysql_options.password}'; mysql -u${SQL_data?.mysql_options.user} --port=${SQL_data?.mysql_options.port} ${dbName} < ${file}`; + const PG = `psql -U ${SQL_data?.pg_options.user} -d "${dbName}" -f "${file}" -p ${SQL_data?.pg_options.port}`; + const MYSQL = `mysql -u ${SQL_data?.mysql_options.user} --port=${SQL_data?.mysql_options.port} ${dbName} < ${file}`; if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) return PG; if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) return MYSQL; return 'invalid dbtype'; }, - + // import TAR file into new DB created runTARFunc: function runTARFunc(dbName, file, dbType: DBType) { const SQL_data = docConfig.getFullConfig(); - const PG = `PGPASSWORD=${SQL_data?.pg_options.password} pg_restore -U ${SQL_data?.pg_options.user} -p ${SQL_data?.pg_options.port} -d "${dbName}" "${file}" `; - const MYSQL = `export MYSQL_PWD='${SQL_data?.mysql_options.password}'; mysqldump -u ${SQL_data?.mysql_options.user} --port=${SQL_data?.mysql_options.port} ${dbName} > ${file}`; + const PG = `pg_restore -U ${SQL_data?.pg_options.user} -p ${SQL_data?.pg_options.port} -d "${dbName}" "${file}" `; + const MYSQL = `mysqldump -u ${SQL_data?.mysql_options.user} --port=${SQL_data?.mysql_options.port} ${dbName} > ${file}`; if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) return PG; if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) return MYSQL; return 'invalid dbtype'; @@ -97,8 +97,8 @@ const helperFunctions: HelperFunctions = { dbType: DBType, ) { const SQL_data = docConfig.getFullConfig(); - const PG = `PGPASSWORD=${SQL_data?.pg_options.password} pg_dump -s -U ${SQL_data?.pg_options.user} -p ${SQL_data?.pg_options.port} -Fp -d ${dbCopyName} > "${newFile}"`; - const MYSQL = `export MYSQL_PWD='${SQL_data?.mysql_options.password}'; mysqldump -h localhost -u ${SQL_data?.mysql_options.user} ${dbCopyName} > ${newFile}`; + const PG = `pg_dump -s -U ${SQL_data?.pg_options.user} -p ${SQL_data?.pg_options.port} -Fp -d ${dbCopyName} > "${newFile}"`; + const MYSQL = `mysqldump -h localhost -u ${SQL_data?.mysql_options.user} ${dbCopyName} > ${newFile}`; if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) return PG; if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) return MYSQL; return 'invalid dbtype'; @@ -109,23 +109,37 @@ const helperFunctions: HelperFunctions = { dbCopyName, file, dbType: DBType, + ) { const SQL_data = docConfig.getFullConfig(); - const PG = ` PGPASSWORD=${SQL_data?.pg_options.password} pg_dump -s -U ${SQL_data?.pg_options.user} -p ${SQL_data?.pg_options.port} -F p -d "${dbCopyName}" > "${file}"`; - const MYSQL = `export MYSQL_PWD='${SQL_data?.mysql_options.password}'; mysqldump -h localhost -u ${SQL_data?.mysql_options.user} --port=${SQL_data?.mysql_options.port} ${dbCopyName} > ${file}`; + const PG = `pg_dump -s -U ${SQL_data?.pg_options.user} -p ${SQL_data?.pg_options.port} -F p -d "${dbCopyName}" > "${file}"`; + const MYSQL = `mysqldump -h localhost -u ${SQL_data?.mysql_options.user} --port=${SQL_data?.mysql_options.port} ${dbCopyName} > ${file}`; if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) return PG; if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) return MYSQL; return 'invalid dbtype'; }, // promisified execute to execute commands in the child process - promExecute: (cmd: string) => + promExecute: (cmd: string, dbType: DBType) => new Promise((resolve, reject) => { - exec( + const SQL_data = docConfig.getFullConfig(); + + let envPW = {}; + + if (dbType === DBType.Postgres || dbType === DBType.RDSPostgres) { + envPW = { PGPASSWORD: SQL_data?.pg_options.password }; + } else if (dbType === DBType.MySQL || dbType === DBType.RDSMySQL) { + envPW = { MYSQL_PWD: SQL_data?.mysql_options.password }; + } + + + exec( // opens cli cmd, { - timeout: 5000, - // env: { PGPASSWORD: docConfig.getFullConfig().pg.password }, + timeout: 2500, + // env: {PGPASSWORD: SQL_data?.pg_options.password }, + env: envPW, + }, (error, stdout, stderr) => { if (error) { @@ -139,4 +153,4 @@ const helperFunctions: HelperFunctions = { }), }; -export default helperFunctions; +export default helperFunctions; \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 00000000..450d3c1e --- /dev/null +++ b/config.json @@ -0,0 +1 @@ +{"mysql_options":{"user":"root","password":"","port":"3306"},"pg_options":{"user":"postgres","password":"Skittle5$s","port":"5434"},"rds_mysql_options":{"user":"","password":"","port":"","host":""},"rds_pg_options":{"user":"","password":"","port":"","host":""},"sqlite_options":{"filename":""},"directPGURI_options":{"connectionString":""}} \ No newline at end of file diff --git a/frontend/components/modal/AddNewDbModalCorrect.tsx b/frontend/components/modal/AddNewDbModalCorrect.tsx index 6c9282cf..803fa6c4 100644 --- a/frontend/components/modal/AddNewDbModalCorrect.tsx +++ b/frontend/components/modal/AddNewDbModalCorrect.tsx @@ -1,4 +1,5 @@ import path from 'path'; +import * as fs from 'fs' import React, { useContext, useState } from 'react'; import { Dialog, DialogTitle, Tooltip } from '@mui/material/'; import CloudUploadIcon from '@mui/icons-material/CloudUpload'; @@ -14,6 +15,8 @@ import { } from '../../style-variables'; import { DBType } from '../../../backend/BE_types'; import MenuContext from '../../state_management/Contexts/MenuContext'; +// import { set } from 'mongoose'; + type AddNewDbModalProps = { open: boolean; @@ -30,9 +33,16 @@ function AddNewDbModal({ }: AddNewDbModalProps) { const { dispatch: menuDispatch } = useContext(MenuContext); - const [newDbName, setNewDbName] = useState(''); - const [isError, setIsError] = useState(false); - const [isEmpty, setIsEmpty] = useState(true); + const [newDbName, setNewDbName] = useState(''); + + const [fileSelect, setFileSelect] = useState(true) + + const [selectedFilePath, setFilePath] = useState('') + + const [selectedDBType, setDBType] = useState('') + + const [isError, setIsError] = useState(false); + const [isEmpty, setIsEmpty] = useState(true); // const [curDBType, setDBType] = useState(); // Resets state for error messages @@ -53,7 +63,7 @@ function AddNewDbModal({ return ''; }; - // Set schema name + /// / Set schema name const handleDbName = (event: React.ChangeEvent) => { const dbNameInput = event.target.value; if (dbNameInput.length === 0) { @@ -73,45 +83,143 @@ function AddNewDbModal({ setNewDbName(dbSafeName); }; - // Opens modal to select file and sends the selected file to backend - const handleDBimport = (dbName: string, closeModal: () => void) => { - // TODO: fix the any type. - const dbt: DBType = (document.getElementById('dbTypeDropdown') as any) - .value; - const options = { - title: 'Import DB', - defaultPath: path.join(__dirname, '../assets/'), - buttonLabel: 'Import', - filters: [ - { - name: 'Custom File Type', - extensions: ['sql', 'tar'], + + + // Opens modal to select file + const selectDBFile = () => { + const options = { + title: 'Select DB File', + defaultPath: path.join(__dirname, '../assets/'), + buttonLabel: 'Select File', + filters: [ + { + name: 'Custom File Type', + extensions: ['sql', 'tar'], + }, + ], + }; + + + // checks sql file if it already has a `CREATE DATABASE` query. If so, a input field wont be needed. + // if there is no such query, you will need to input a db name. + const checkDBFile = (filePath: string, dbName: string) => { + + // TODO: fix the any type. + const dbt: DBType = (document.getElementById('dbTypeDropdown') as any).value; + + console.log('dbtype', dbt) + console.log('filepath', filePath) + + setFilePath(filePath) + + setDBType(dbt) + + fs.readFile(filePath, 'utf-8', (err, data)=> { + if(err) { + console.error(`Error reading file: ${err.message}`); + return; + } + + // this is for sql files that already have a name via CREATE DATABASE + const dataArr = data.replace(/`([^`]+)`|\b([a-zA-Z_]+)\b/g, '$1$2').replace(/;/g, '').match(/\S+/g) || []; + const keyword1 = 'CREATE'; + const keyword2 = 'DATABASE'; + const keyword3 = 'USE' + console.log('data', dataArr) + + const containsKeywords = dataArr.some((word, index) => { + // Check if the current word is 'CREATE' and the next word is 'DATABASE' + if (word === keyword1 && dataArr[index + 1] === keyword2) { + return true; + } + return false; + }); + + /* checks if the keyword exist in our database file */ + if(containsKeywords) { + let fileDbName = '' + let payloadObj + console.log('keywords exist:', containsKeywords); + + // mysql is different where you need to create a database before importing. + // most mysql files will have a create database query in file + // this function will create a database first + if (dbt === DBType.Postgres) { + + // eslint-disable-next-line no-restricted-syntax + for (const [index, word] of dataArr.entries()) { + if (word === keyword1 && dataArr[index + 1] === keyword2) { + // Assuming the database name is the next word after 'DATABASE' + fileDbName = dataArr[index + 2]; + } + } + payloadObj = { newDbName, filePath, dbType: dbt}; + } else if (dbt === DBType.MySQL) { + + // eslint-disable-next-line no-restricted-syntax + for (const [index, word] of dataArr.entries()) { + if (word === keyword3) { + // Assuming the database name is the next word after 'DATABASE' + fileDbName = dataArr[index + 1]; + } + } + payloadObj = { newDbName: fileDbName, filePath, dbType: dbt}; + } + + + // handles import if keywords exists + const handleDBImport = (closeModal: () => void) => { + menuDispatch({ + type: 'ASYNC_TRIGGER', + loading: 'LOADING', + options: { + event: 'import-db', + payload: payloadObj, + callback: closeModal, + }, + }); + }; + + handleDBImport(handleClose) + + } else { + // if keywords dont exist, this will render input field + setFileSelect(false) + + console.log('keywords exist:', containsKeywords); + } + }); + } + + // initial async call when pressing select file button + menuDispatch({ + type: 'ASYNC_TRIGGER', + loading: 'LOADING', + options: { + event: 'showOpenDialog', + payload: options, + callback: checkDBFile, }, - ], + }); }; - // this runs after opendialog resolves, use as callback - const importdb = (filePath: string) => { + + + // some sql files will have keywords that are invalid which will need to be edited manually in sql file before importing + const handleDBImport = (dbName: string, closeModal: () => void) => { menuDispatch({ type: 'ASYNC_TRIGGER', loading: 'LOADING', options: { event: 'import-db', - payload: { newDbName: dbName, filePath, dbType: dbt }, // see importDb for type reqs + payload: { newDbName: dbName, filePath: selectedFilePath , dbType: selectedDBType}, callback: closeModal, }, }); + setFileSelect(true) }; - // initial async call - menuDispatch({ - type: 'ASYNC_TRIGGER', - loading: 'LOADING', - options: { - event: 'showOpenDialog', - payload: options, - callback: importdb, - }, - }); - }; + + + return (
@@ -126,6 +234,8 @@ function AddNewDbModal({ Import Existing SQL or TAR File + + {!fileSelect ? + : <> + } + + Cancel + + + { + fileSelect ? + } + onClick={ + () => selectDBFile() + } + + > + Select File + + : {} - : () => handleDBimport(newDbName, handleClose) + : () => handleDBImport(newDbName, handleClose) } > Import + } + +
diff --git a/frontend/components/sidebar/QueryEntry.tsx b/frontend/components/sidebar/QueryEntry.tsx index b7f6d0ab..bbecfba8 100644 --- a/frontend/components/sidebar/QueryEntry.tsx +++ b/frontend/components/sidebar/QueryEntry.tsx @@ -46,7 +46,7 @@ function QueryEntry({ }: QueryEntryProps) { return ( - + diff --git a/frontend/components/views/DbView/DatabaseDetails.tsx b/frontend/components/views/DbView/DatabaseDetails.tsx index 708b5675..7727aaea 100644 --- a/frontend/components/views/DbView/DatabaseDetails.tsx +++ b/frontend/components/views/DbView/DatabaseDetails.tsx @@ -17,10 +17,10 @@ function DatabaseDetails({ db }: DatabaseDetailsProps) { if (!db) return null; return ( - - {`Database Name: ${db.db_name}`} -
- {`Database Size: ${db.db_size}`} + + {`Database: ${db.db_name} (${db.db_size})`} + {/*
*/} + {/* {`Database Size: ${db.db_size}`} */}
); diff --git a/frontend/components/views/DbView/TableDetails.tsx b/frontend/components/views/DbView/TableDetails.tsx index 44281b14..c3d7d87d 100644 --- a/frontend/components/views/DbView/TableDetails.tsx +++ b/frontend/components/views/DbView/TableDetails.tsx @@ -1,4 +1,6 @@ +import { ipcRenderer } from 'electron'; import React from 'react'; +import { useEffect, useState } from 'react'; import { Table, TableBody, @@ -12,6 +14,7 @@ import { import styled from 'styled-components'; import { greyDark, greyPrimary } from '../../../style-variables'; import { TableInfo } from '../../../types'; +import { DBType } from '../../../../backend/BE_types'; const StyledPaper = styled(({ ...other }) => ( @@ -25,45 +28,64 @@ const StyledCell = styled(TableCell)` interface TableDetailsProps { table: TableInfo | undefined; + selectedDb: string; + curDBType: DBType | undefined; } -function TableDetails({ table }: TableDetailsProps) { - return <> - {`${table?.table_name}`} -
- - - - - - Column - - - Type - - - Is Nullable? - - - - - {table?.columns.map((row) => ( - - {row?.column_name} - - {`${row?.data_type}${ - row?.character_maximum_length - ? `(${row.character_maximum_length})` - : '' - }`} - - {row?.is_nullable} +function TableDetails({ table, selectedDb, curDBType }: TableDetailsProps) { + const [data,setData] = useState([]) + const onDisplay = () => { + ipcRenderer + .invoke( + 'run-select-all-query', + { + sqlString: `SELECT * FROM ${table?.table_name}`, + selectedDb, + }, + curDBType, + ) + .then((data) => { + setData(data) + }) + .catch((err) => { + console.error('Error in onDisplay ', err); + }); + }; + + useEffect(() => { + onDisplay(); + }, [table, selectedDb, curDBType]); + + return ( + <> + {`${table?.table_name}`} +
+ +
+ + + {table?.columns.map((row) => ( + + {`${row?.column_name} (${row?.data_type} ${row.character_maximum_length})`} + + ))} - ))} - -
-
- + + + {data?.map((element) => ( + + {Object.keys(element).map((column) => ( + + {element[column]} + + ))} + + ))} + + + + + ); } export default TableDetails; diff --git a/frontend/components/views/DbView/TablesTabBar.tsx b/frontend/components/views/DbView/TablesTabBar.tsx index 6d4d32e1..b8e9a65d 100644 --- a/frontend/components/views/DbView/TablesTabBar.tsx +++ b/frontend/components/views/DbView/TablesTabBar.tsx @@ -1,32 +1,81 @@ -import React, { useState } from 'react'; -import { Tabs, Tab } from '@mui/material'; +import fs from 'fs'; +import { ipcRenderer } from 'electron'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Tabs, Tab, Button } from '@mui/material'; import ToggleButton from '@mui/material/ToggleButton'; -import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +// import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import SaveAsIcon from '@mui/icons-material/SaveAs'; +import PlaylistAddIcon from '@mui/icons-material/PlaylistAdd'; +import AccountTreeIcon from '@mui/icons-material/AccountTree'; +import RestorePageIcon from '@mui/icons-material/RestorePage'; +import ReactFlow, { + applyEdgeChanges, + applyNodeChanges, + Background, + Controls, + Edge, + MiniMap, + Node, +} from 'reactflow'; +import 'reactflow/dist/style.css'; import styled from 'styled-components'; +import { greyPrimary, greenPrimary } from '../../../style-variables'; import TableDetails from './TableDetails'; import { AppState, TableInfo } from '../../../types'; import { DBType } from '../../../../backend/BE_types'; -import { greyPrimary, greenPrimary } from '../../../style-variables'; -import ERTables from '../ERTables/ERTabling'; +// import ERTables from '../ERTables/ERTabling'; +import stateToReactFlow from '../../../lib/convertStateToReactFlow'; +import { + AddTablesObjType, + SchemaStateObjType, + TableHeaderNodeType, + UpdatesObjType, +} from '../../../types'; +import nodeTypes from '../ERTables/NodeTypes'; +import * as colors from '../../../style-variables'; +//This is apart of the table view interface TabPanelProps { children?: React.ReactNode; index: number; value: number; - /* curDBType: DBType | undefined; */ } -const StyledToggleButtonGroup = styled(ToggleButtonGroup)` - background-color: ${greenPrimary}; - margin-bottom: 10px; + +// const StyledToggleButtonGroup = styled(ToggleButtonGroup)` +// background-color: ${greenPrimary}; +// margin-bottom: 1em; +// `; + +const StyledViewButton = styled(Button)` + margin: 1em 0em 0em 1em; + padding: 0.2em; `; +const StyledToggleButton = styled(ToggleButton)` + background-color: ${greenPrimary}; + margin: 0em 0.5em 1em 0em; + padding: 0.2em 1em; + font-size: 1em; + box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), + 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12); + &:hover { + background-color: #11774e; + } +`; const StyledTabs = styled(Tabs)` background-color: ${greyPrimary}; color: white; border-radius: 5px; `; -function TabPanel({ children, value, index /* curDBType */ }: TabPanelProps) { +//This is apart of the table view +function TabPanel({ + children, + value, + index, +}: // selectedDb, +// curDBType, +TabPanelProps) { return (
{ + switch (node.type) { + case 'tableHeader': + return colors.greyLightest; + case 'tableField': + return 'white'; + case 'tableFooter': + return 'white'; + default: + return 'red'; + } +}; + +type ERTablingProps = { + tables: TableInfo[]; + selectedDb: AppState['selectedDb']; + curDBType: DBType | undefined; +}; + const a11yProps = (index: number) => ({ id: `scrollable-auto-tab-${index}`, 'aria-controls': `scrollable-auto-tabpanel-${index}`, @@ -57,63 +142,6 @@ interface HandleChangeFunc { (event: React.ChangeEvent, newValue: number): void; } -interface ErViewProps { - active: boolean; - tables: TableInfo[]; - selectedDb: AppState['selectedDb']; - curDBType: DBType | undefined; - tableIndex: number; - handleChange: HandleChangeFunc; -} - -function ErView({ - active, - tables, - selectedDb, - curDBType, - tableIndex, - handleChange, -}: ErViewProps) { - return ( -
- {active ? ( - - ) : ( - <> - - {tables.map(({ table_name: name }, index: number) => ( - - ))} - ; - -
-
- {tables.map((tableMap, index) => ( - - - - ))} - - )} -
- ); -} function TablesTabs({ tables, @@ -122,7 +150,198 @@ function TablesTabs({ selectedDb, setERView, curDBType, -}: TablesTabBarProps) { +}: TablesTabBarProps & ERTablingProps) { + console.log(tables); + console.log(selectTable); + console.log(selectedTable); + console.log(setERView); + console.log(curDBType); + console.log(selectedDb); + //react flow functions to save layout + interface FlowType { + toObject(): any; + } + const [nodes, setNodes] = useState([]); + const [edges, setEdges] = useState([]); + + // state for custom controls toggle & saves a copy of the schema + // when tables (which is the database that is selected changes, update SchemaState) + const [schemaState, setSchemaState] = useState({ + database: 'initial', + tableList: [], + }); + + 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, + }); + // whenever the selectedDb changes, reassign the backendObj to contain this selectedDb + useEffect(() => { + backendObj.current.database = selectedDb; + }, [selectedDb]); + + // 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 + function handleAddTable(): void { + const schemaStateString = JSON.stringify(schemaState); + const schemaStateCopy = JSON.parse(schemaStateString); + console.log(schemaStateCopy); + // 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); + console.log(schemaStateCopy); + // set the state, which worries about the table positions. + setSchemaState(schemaStateCopy); + } + + // This function is supposed to handle the layout saving of the positions of the tables. + const handleSaveLayout = async (): Promise => { + // get the array of header nodes + const headerNodes = nodes.filter( + (node) => node.type === 'tableHeader', + ) as TableHeaderNodeType[]; + // create object for the current database + console.log('header nodes', headerNodes); + + type TablePosObjType = { + id: string; + table_position: { + x: number; + y: number; + }; + }; + + // just defines the type + type DatabaseLayoutObjType = { + db_name: string; + db_tables: TablePosObjType[]; + }; + + // initializes a variable using the type. + const currDatabaseLayout: DatabaseLayoutObjType = { + db_name: backendObj.current.database, + db_tables: [], + }; + + // populate the db_tables property for the database + headerNodes.forEach((node) => { + const tablePosObj: TablePosObjType = { + id: node.id, + table_position: { x: node.position.x, y: node.position.y }, + }; + currDatabaseLayout.db_tables.push(tablePosObj); + }); + console.log('currdblayout', currDatabaseLayout); + + // create an array of objects in local storage containing table info from currDatabaselayout + const layoutFlowKey = 'layout-key'; + + const existingLayouts: DatabaseLayoutObjType[] = JSON.parse( + localStorage.getItem(layoutFlowKey) ?? '[]', + ); // ?? returns [] if null/notfound (arr of objs) + + // check if dbname in existinglayouts is equal to currdatabaylayout name (when new db is first initialized/not found, findIndex will return -1) + const existingLayoutIndex = existingLayouts.findIndex( + (layout) => layout.db_name === currDatabaseLayout.db_name, + ); + console.log('layoutindex', existingLayoutIndex); + if (existingLayoutIndex !== -1) { + existingLayouts[existingLayoutIndex] = currDatabaseLayout; // if it exists, if so, updates to new postions + } else { + existingLayouts.push(currDatabaseLayout); // if not exists, pushes it to array + } + + localStorage.setItem(layoutFlowKey, JSON.stringify(existingLayouts)); + console.log('nodes', nodes); + // current problem: doesnt remove deleted databases from existingLayouts value + }; + + // When you click the save button, you save the layout of the tables and you send a very large object to the backend containing all of the changes. + function handleClickSave(): void { + // This function sends a message to the back end with + // the data in backendObj.current + handleSaveLayout(); + + ipcRenderer + .invoke('ertable-schemaupdate', backendObj.current, selectedDb, curDBType) + .then(async () => { + // resets the backendObj + backendObj.current = { + database: schemaState.database, + updates, + }; + }) + .catch((err: object) => { + console.log(err); + }); + } + + + // This useEffect fires when schemaState changes and will convert the state to a form react flow requires + useEffect(() => { + // console.log(schemaState); + // console.log(stateToReactFlow); + // send the schema state to the convert method to convert the schema to the form react flow requires + const initialState = stateToReactFlow.convert(schemaState); + // console.log(initialState); + // 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]); + //end of the schema state + + + + // End of ERTabling / ERD View const handleChange: HandleChangeFunc = (event, newValue) => { selectTable(tables[newValue]); }; @@ -146,38 +365,128 @@ function TablesTabs({ } } }; - return ( -
- - +
+
+ ER diagram - - + - Table - - - + Table View + +
+ + {active ? ( + + + + + + + + + + + + + + + + + + + ) : ( + <> + + {tables.map(({ table_name: name }, index: number) => ( + + ))} + + +
+
+ + {tables.map((tableMap, index) => ( + + + + + ))} + + )}
); } - export default TablesTabs; diff --git a/frontend/components/views/ERTables/ERDisplayWindow.tsx b/frontend/components/views/ERTables/ERDisplayWindow.tsx index 9517235f..d2ad9985 100644 --- a/frontend/components/views/ERTables/ERDisplayWindow.tsx +++ b/frontend/components/views/ERTables/ERDisplayWindow.tsx @@ -1,335 +1,335 @@ -import fs from 'fs'; -import { Button } from '@mui/material'; -import { ipcRenderer } from 'electron'; -import React, { - useCallback, - useEffect, - useRef, - useState, - useReducer, -} from 'react'; -import ReactFlow, { - applyEdgeChanges, - applyNodeChanges, - Background, - Controls, - Edge, - MiniMap, - Node, -} from 'reactflow'; -import 'reactflow/dist/style.css'; -import styled from 'styled-components'; -import { DBType } from '../../../../backend/BE_types'; -import stateToReactFlow from '../../../lib/convertStateToReactFlow'; -import { - AddTablesObjType, - AppState, - SchemaStateObjType, - TableHeaderNodeType, - TableInfo, - UpdatesObjType, -} from '../../../types'; -import nodeTypes from './NodeTypes'; -import { ErdUpdatesType } from '../../../../shared/types/erTypes'; -import { - mainErdReducer, - initialErdState, -} from '../../../state_management/Reducers/ERDReducers'; -import * as PostgresActions from '../../../state_management/Actions/ERDPsqlActions'; -import * as MySqlActions from '../../../state_management/Actions/ERDMySqlActions'; -import * as SqLiteActions from '../../../state_management/Actions/ERDSqLiteActions'; +// import fs from 'fs'; +// import { Button } from '@mui/material'; +// import { ipcRenderer } from 'electron'; +// import React, { +// useCallback, +// useEffect, +// useRef, +// useState, +// useReducer, +// } from 'react'; +// import ReactFlow, { +// applyEdgeChanges, +// applyNodeChanges, +// Background, +// Controls, +// Edge, +// MiniMap, +// Node, +// } from 'reactflow'; +// import 'reactflow/dist/style.css'; +// import styled from 'styled-components'; +// import { DBType } from '../../../../backend/BE_types'; +// import stateToReactFlow from '../../../lib/convertStateToReactFlow'; +// import { +// AddTablesObjType, +// AppState, +// SchemaStateObjType, +// TableHeaderNodeType, +// TableInfo, +// UpdatesObjType, +// } from '../../../types'; +// import nodeTypes from './NodeTypes'; +// import { ErdUpdatesType } from '../../../../shared/types/erTypes'; +// import { +// mainErdReducer, +// initialErdState, +// } from '../../../state_management/Reducers/ERDReducers'; +// import * as PostgresActions from '../../../state_management/Actions/ERDPsqlActions'; +// import * as MySqlActions from '../../../state_management/Actions/ERDMySqlActions'; +// import * as SqLiteActions from '../../../state_management/Actions/ERDSqLiteActions'; -import * as colors from '../../../style-variables'; +// import * as colors from '../../../style-variables'; -/** - * FRONTEND COSMETIC STUFF - */ +// /** +// * FRONTEND COSMETIC STUFF +// */ -// defines the styling for the ERDiagram window -const rfStyle: object = { - height: '65vh', - border: `2px solid ${colors.greenPrimary}`, - borderRadius: '0.3rem', -}; +// // defines the styling for the ERDiagram window +// const rfStyle: object = { +// height: '65vh', +// border: `2px solid ${colors.greenPrimary}`, +// borderRadius: '0.3rem', +// }; -// defines the styling for the minimap -const mmStyle: object = { - backgroundColor: colors.bgColor, - border: `2px solid ${colors.greenPrimary}`, - borderRadius: '0.3rem', - height: 150, - overflow: 'hidden', -}; +// // defines the styling for the minimap +// const mmStyle: object = { +// backgroundColor: colors.bgColor, +// border: `2px solid ${colors.greenPrimary}`, +// borderRadius: '0.3rem', +// height: 150, +// overflow: 'hidden', +// }; -// defines the styling for the minimap nodes -const nodeColor = (node: Node): string => { - switch (node.type) { - case 'tableHeader': - return colors.greyLightest; - case 'tableField': - return 'white'; - default: - return 'red'; - } -}; +// // defines the styling for the minimap nodes +// const nodeColor = (node: Node): string => { +// switch (node.type) { +// case 'tableHeader': +// return colors.greyLightest; +// case 'tableField': +// return 'white'; +// default: +// return 'red'; +// } +// }; -type ERTablingProps = { - tables: TableInfo[]; - selectedDb: AppState['selectedDb']; - curDBType: DBType | undefined; -}; +// type ERTablingProps = { +// tables: TableInfo[]; +// selectedDb: AppState['selectedDb']; +// curDBType: DBType | undefined; +// }; -const StyledViewButton = styled(Button)` - margin: 1rem; - margin-left: 0rem; - font-size: 0.78em; - padding: 0.45em; -`; +// const StyledViewButton = styled(Button)` +// margin: 1rem; +// margin-left: 0rem; +// font-size: 0.78em; +// padding: 0.45em; +// `; -/** - * ACTION MAP - * This is dynamic since we imported as * we have access to all functions! - */ +// /** +// * ACTION MAP +// * This is dynamic since we imported as * we have access to all functions! +// */ -const actionMap = { - [DBType.Postgres]: PostgresActions, - [DBType.MySQL]: MySqlActions, - [DBType.SQLite]: SqLiteActions, -}; +// const actionMap = { +// [DBType.Postgres]: PostgresActions, +// [DBType.MySQL]: MySqlActions, +// [DBType.SQLite]: SqLiteActions, +// }; -/** - * MAIN FUNCTION - * */ -function ERTabling({ tables, selectedDb, curDBType }: ERTablingProps) { - const [schemaState, setSchemaState] = useState({ - database: 'initial', - tableList: [], - }); - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); +// /** +// * MAIN FUNCTION +// * */ +// function ERTabling({ tables, selectedDb, curDBType }: ERTablingProps) { +// const [schemaState, setSchemaState] = useState({ +// database: 'initial', +// tableList: [], +// }); +// const [nodes, setNodes] = useState([]); +// const [edges, setEdges] = useState([]); - /** - * USEREDUCER - * */ - const [erdState, erdDispatch] = useReducer(mainErdReducer, initialErdState); +// /** +// * USEREDUCER +// * */ +// const [erdState, erdDispatch] = useReducer(mainErdReducer, initialErdState); - // state for custom controls toggle - // when tables (which is the database that is selected changes, update SchemaState) - useEffect(() => { - setSchemaState({ database: selectedDb, tableList: tables }); - }, [tables, selectedDb]); +// // state for custom controls toggle +// // 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, - }); +// // 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, +// }); - // NEW updates array - const erdUpdatesArray: ErdUpdatesType = []; +// // NEW updates array +// const erdUpdatesArray: ErdUpdatesType = []; - //define useReducer for all actions that can trigger from table +// //define useReducer for all actions that can trigger from table - // whenever the selectedDb changes, reassign the backendObj to contain this selectedDb - useEffect(() => { - backendObj.current.database = selectedDb; - console.log('backendObj: ', backendObj); +// // whenever the selectedDb changes, reassign the backendObj to contain this selectedDb +// useEffect(() => { +// backendObj.current.database = selectedDb; +// console.log('backendObj: ', backendObj); - // backendColumnObj.current.database = selectedDb; - }, [selectedDb]); +// // backendColumnObj.current.database = selectedDb; +// }, [selectedDb]); - // 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], - ); +// // 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); - }; +// // 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 = async (): Promise => { - // get the array of header nodes - const headerNodes = nodes.filter( - (node) => node.type === 'tableHeader', - ) as TableHeaderNodeType[]; - // create object for the current database +// const handleSaveLayout = async (): Promise => { +// // 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 TablePosObjType = { +// table_name: string; +// table_position: { +// x: number; +// y: number; +// }; +// }; - type DatabaseLayoutObjType = { - db_name: string; - db_tables: TablePosObjType[]; - }; +// type DatabaseLayoutObjType = { +// db_name: string; +// db_tables: TablePosObjType[]; +// }; - const currDatabaseLayout: DatabaseLayoutObjType = { - db_name: backendObj.current.database, - db_tables: [], - }; +// 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); - }); +// // 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 = await ipcRenderer.invoke('get-path', 'temp'); - const filePath = location.concat('/UserTableLayouts.json'); +// const location: string = await ipcRenderer.invoke('get-path', 'temp'); +// const filePath = location.concat('/UserTableLayouts.json'); - fs.readFile(filePath, 'utf-8', (err, data) => { - // check if error exists (no file found) - if (err) { - fs.writeFile( - filePath, - 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); +// fs.readFile(filePath, 'utf-8', (err, data) => { +// // check if error exists (no file found) +// if (err) { +// fs.writeFile( +// filePath, +// 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(filePath, JSON.stringify(dbLayouts, null, 2), (error) => { - if (error) console.log(error); - }); - } - }); - }; - function handleClickSave(): void { - // This function sends a message to the back end with - // the data in backendObj.current - handleSaveLayout(); - ipcRenderer - .invoke('ertable-schemaupdate', backendObj.current, selectedDb, curDBType) - .then(async () => { - // resets the backendObj - console.log('inside the handleClick save', backendObj.current); - backendObj.current = { - database: schemaState.database, - updates, - }; - }) - .catch((err: object) => { - console.log(err); - }); - } +// // write changes to the file +// fs.writeFile(filePath, JSON.stringify(dbLayouts, null, 2), (error) => { +// if (error) console.log(error); +// }); +// } +// }); +// }; +// function handleClickSave(): void { +// // This function sends a message to the back end with +// // the data in backendObj.current +// handleSaveLayout(); +// ipcRenderer +// .invoke('ertable-schemaupdate', backendObj.current, selectedDb, curDBType) +// .then(async () => { +// // resets the backendObj +// console.log('inside the handleClick save', backendObj.current); +// backendObj.current = { +// database: schemaState.database, +// updates, +// }; +// }) +// .catch((err: object) => { +// console.log(err); +// }); +// } - // 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]); +// // 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]); - return ( -
- - {' '} - Add New Table{' '} - - - {' '} - Save{' '} - - - - - - -
- ); -} +// return ( +//
+// +// {' '} +// Add New Table{' '} +// +// +// {' '} +// Save{' '} +// +// +// +// +// +// +//
+// ); +// } -export default ERTabling; +// export default ERTabling; diff --git a/frontend/components/views/ERTables/ERTabling.tsx b/frontend/components/views/ERTables/ERTabling.tsx deleted file mode 100644 index e98f37bc..00000000 --- a/frontend/components/views/ERTables/ERTabling.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import fs from 'fs'; -import { Button } from '@mui/material'; -import { ipcRenderer } from 'electron'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import ReactFlow, { - applyEdgeChanges, - applyNodeChanges, - Background, - Controls, - Edge, - MiniMap, - Node, -} from 'reactflow'; -import 'reactflow/dist/style.css'; -import styled from 'styled-components'; -import { DBType } from '../../../../backend/BE_types'; -import stateToReactFlow from '../../../lib/convertStateToReactFlow'; -import { - AddTablesObjType, - AppState, - SchemaStateObjType, - TableHeaderNodeType, - TableInfo, - UpdatesObjType, -} from '../../../types'; -import nodeTypes from './NodeTypes'; - -import * as colors from '../../../style-variables'; - -// defines the styling for the ERDiagram window -const rfStyle: object = { - height: '65vh', - border: `2px solid ${colors.greenPrimary}`, - borderRadius: '0.3rem', -}; - -// defines the styling for the minimap -const mmStyle: object = { - backgroundColor: colors.bgColor, - border: `2px solid ${colors.greenPrimary}`, - borderRadius: '0.3rem', - height: 150, - overflow: 'hidden', -}; - -// defines the styling for the minimap nodes -const nodeColor = (node: Node): string => { - switch (node.type) { - case 'tableHeader': - return colors.greyLightest; - case 'tableField': - return 'white'; - default: - return 'red'; - } -}; - -type ERTablingProps = { - tables: TableInfo[]; - selectedDb: AppState['selectedDb']; - curDBType: DBType | undefined; -}; - -const StyledViewButton = styled(Button)` - margin: 1rem; - margin-left: 0rem; - font-size: 0.78em; - padding: 0.45em; -`; - -// the ERTabling componenet is what deals with the ER Diagram view and it's positioning. All of this gets converted to react flow, as for the backendObj, this is what gets sent to the backend to run all the queries. -function ERTabling({ tables, selectedDb, curDBType }: ERTablingProps) { - const [schemaState, setSchemaState] = useState({ - database: 'initial', - tableList: [], - }); - const [nodes, setNodes] = useState([]); - const [edges, setEdges] = useState([]); - - // state for custom controls toggle - // 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, - }); - - // whenever the selectedDb changes, reassign the backendObj to contain this selectedDb - useEffect(() => { - backendObj.current.database = selectedDb; - }, [selectedDb]); - - // 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, which worries about the table positions. - setSchemaState(schemaStateCopy); - }; - - // This function is supposed to handle the layout saving of the positions of the tables. - const handleSaveLayout = async (): Promise => { - // 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); - }); - - // what this is doing is it's creating a json file in your temp folder and saving the layout of the tables in there. so positioning is all saved locally. - const location: string = await ipcRenderer.invoke('get-path', 'temp'); - const filePath = location.concat('/UserTableLayouts.json'); - - fs.readFile(filePath, 'utf-8', (err, data) => { - // check if error exists (no file found) - if (err) { - fs.writeFile( - filePath, - 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(filePath, JSON.stringify(dbLayouts, null, 2), (error) => { - if (error) console.log(error); - }); - } - }); - }; - - // When you click the save button, you save the layout of the tables and you send a very large object to the backend containing all of the changes. - function handleClickSave(): void { - // This function sends a message to the back end with - // the data in backendObj.current - handleSaveLayout(); - ipcRenderer - .invoke('ertable-schemaupdate', backendObj.current, selectedDb, curDBType) - .then(async () => { - // resets the backendObj - backendObj.current = { - database: schemaState.database, - updates, - }; - }) - .catch((err: object) => { - console.log(err); - }); - } - - // 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]); - - return ( -
- - {' '} - Add New Table{' '} - - - {' '} - Save{' '} - - - - - - -
- ); -} - -export default ERTabling; diff --git a/frontend/components/views/ERTables/NodeTypes.ts b/frontend/components/views/ERTables/NodeTypes.ts index 5812c4cb..fdd273d8 100644 --- a/frontend/components/views/ERTables/NodeTypes.ts +++ b/frontend/components/views/ERTables/NodeTypes.ts @@ -1,5 +1,6 @@ import tableHeader from './TableHeaderNode'; import tableField from './TableFieldNode'; +import tableFooter from './TableFooterNode'; /** * This file is required for React-flow * React-flow states: @@ -13,10 +14,12 @@ import tableField from './TableFieldNode'; type NodeTypes = { tableHeader: any; tableField: any; + tableFooter: any; }; const nodeTypes: NodeTypes = { tableHeader, tableField, + tableFooter, }; export type TablePosObjType = { table_name: string; diff --git a/frontend/components/views/ERTables/TableFieldNode.tsx b/frontend/components/views/ERTables/TableFieldNode.tsx index b1af073f..084d0d81 100644 --- a/frontend/components/views/ERTables/TableFieldNode.tsx +++ b/frontend/components/views/ERTables/TableFieldNode.tsx @@ -48,6 +48,7 @@ const Accordion = styled((props: AccordionProps) => ( /> ))(() => ({})); + function TableField({ data }: TableFieldProps) { const { schemaStateCopy, @@ -68,6 +69,9 @@ function TableField({ data }: TableFieldProps) { const isNull: string = is_nullable; let setTimeoutVariable; + // used in handleAccordianClick and handleMouseLeave + const [isAccordionExpanded, setAccordionExpanded] = useState(false); + // handles functionality of the drop down delete button const handleDropColumn = (): void => { // iterate through schema copy @@ -117,6 +121,7 @@ function TableField({ data }: TableFieldProps) { }; // handles functionality of the drop down update button const handleUpdateColumn = (): void => { + setAccordionExpanded(!isAccordionExpanded); // create an alterColumns object const alterColumnsObj: AlterColumnsObjType = { column_name, @@ -136,11 +141,7 @@ function TableField({ data }: TableFieldProps) { 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 - ) { + for (let j: number = 0; j < schemaStateCopy.tableList[i].columns.length; j += 1) { if ( schemaStateCopy.tableList[i].columns[j].column_name === column_name ) { @@ -161,20 +162,29 @@ function TableField({ data }: TableFieldProps) { ) as HTMLSelectElement; if (column_name !== columnNameInput.value) { alterColumnsObj.new_column_name = columnNameInput.value; - schemaStateCopy.tableList[i].columns[j].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}`; + schemaStateCopy.tableList[i].columns[j].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'; - schemaStateCopy.tableList[i].columns[j].is_nullable = isNullableString; - alterColumnsObj.is_nullable = isNull !== isNullableString ? isNullableString : null; + const isNullableString: 'YES' | 'NO' = isNullable.checked + ? 'YES' + : 'NO'; + schemaStateCopy.tableList[i].columns[j].is_nullable = + isNullableString; + alterColumnsObj.is_nullable = + isNull !== isNullableString ? isNullableString : null; // handle max_character_length change const columnMaxCharacterLengthInput = document.getElementById( @@ -182,8 +192,8 @@ function TableField({ data }: TableFieldProps) { ) as HTMLSelectElement; if (columnMaxCharacterLengthInput.value) { if ( - character_maximum_length - !== parseInt(columnMaxCharacterLengthInput.value, 10) + character_maximum_length !== + parseInt(columnMaxCharacterLengthInput.value, 10) ) { alterColumnsObj.character_maximum_length = parseInt( columnMaxCharacterLengthInput.value, @@ -203,11 +213,12 @@ function TableField({ data }: TableFieldProps) { `type-dd-${tableColumn}`, ) as HTMLSelectElement; if ( - (data_type === 'character varying' ? 'varchar' : data_type) - !== dataTypeInput.value + (data_type === 'character varying' ? 'varchar' : data_type) !== + dataTypeInput.value ) { alterColumnsObj.data_type = dataTypeInput.value; - schemaStateCopy.tableList[i].columns[j].data_type = dataTypeInput.value; + schemaStateCopy.tableList[i].columns[j].data_type = + dataTypeInput.value; } // handle add/Drop Constraint type @@ -225,8 +236,8 @@ function TableField({ data }: TableFieldProps) { ) as HTMLInputElement; // if constraint type is PK in state but checkbox is unchecked, drop the constraint if ( - constraint_type === 'PRIMARY KEY' - && pkCheckBox.checked === false + constraint_type === 'PRIMARY KEY' && + pkCheckBox.checked === false ) { // modify state to remove constraint schemaStateCopy.tableList[i].columns[j].constraint_type = null; @@ -236,15 +247,19 @@ function TableField({ data }: TableFieldProps) { ); } // if constraint type is not in state but checkbox is checked, add the constraint else if ( - constraint_type !== 'PRIMARY KEY' - && pkCheckBox.checked === true + constraint_type !== 'PRIMARY KEY' && + pkCheckBox.checked === true ) { // modify state to remove constraint - schemaStateCopy.tableList[i].columns[j].constraint_type = 'PRIMARY KEY'; + schemaStateCopy.tableList[i].columns[j].constraint_type = + 'PRIMARY KEY'; // create a copy in case multiple constraints are added - const addConstraintObjCopy: AddConstraintObjType = { ...addConstraintObj }; + const addConstraintObjCopy: AddConstraintObjType = { + ...addConstraintObj, + }; // name the constraint PK_ - addConstraintObjCopy.constraint_name = `pk_${data.tableName + column_name + addConstraintObjCopy.constraint_name = `pk_${ + data.tableName + column_name }`; // assign the constraint_type to 'PRIMARY KEY' addConstraintObjCopy.constraint_type = 'PRIMARY KEY'; @@ -258,8 +273,8 @@ function TableField({ data }: TableFieldProps) { ) as HTMLInputElement; // if constraint type is FK in state but checkbox is unchecked, drop the constraint if ( - constraint_type === 'FOREIGN KEY' - && fkCheckBox.checked === false + constraint_type === 'FOREIGN KEY' && + fkCheckBox.checked === false ) { // modify state to remove constraint schemaStateCopy.tableList[i].columns[j].constraint_type = null; @@ -268,14 +283,16 @@ function TableField({ data }: TableFieldProps) { `FK_${data.tableName + column_name}`, ); } else if ( - constraint_type !== 'FOREIGN KEY' - && fkCheckBox.checked === true + constraint_type !== 'FOREIGN KEY' && + fkCheckBox.checked === true ) { // modify state to add constraint - schemaStateCopy.tableList[i].columns[j].constraint_type = 'FOREIGN KEY'; + schemaStateCopy.tableList[i].columns[j].constraint_type = + 'FOREIGN KEY'; const addConstraintObjCopy = { ...addConstraintObj }; // name the constraint FK_ - addConstraintObjCopy.constraint_name = `fk_${data.tableName + column_name + addConstraintObjCopy.constraint_name = `fk_${ + data.tableName + column_name }`; // assign the constraint type to 'FOREIGN KEY' addConstraintObjCopy.constraint_type = 'FOREIGN KEY'; @@ -296,21 +313,33 @@ function TableField({ data }: TableFieldProps) { } // handle unique constraint - const uniqueCheckBox = document.getElementById(`unique-chkbox-${tableColumn}`) as HTMLInputElement; - if (constraint_type === 'UNIQUE' && uniqueCheckBox.checked === false) { + const uniqueCheckBox = document.getElementById( + `unique-chkbox-${tableColumn}`, + ) as HTMLInputElement; + if ( + constraint_type === 'UNIQUE' && + uniqueCheckBox.checked === false + ) { // modify state to remove constraint schemaStateCopy.tableList[i].columns[j].constraint_type = null; // 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) { + } else if ( + constraint_type !== 'UNIQUE' && + uniqueCheckBox.checked === true + ) { // modify state to add constraint - schemaStateCopy.tableList[i].columns[j].constraint_type = 'UNIQUE'; + schemaStateCopy.tableList[i].columns[j].constraint_type = + 'UNIQUE'; // create a copy in case multiple constraints are added - const addConstraintObjCopy: AddConstraintObjType = { ...addConstraintObj }; + const addConstraintObjCopy: AddConstraintObjType = { + ...addConstraintObj, + }; // name the constraint PK_ - addConstraintObjCopy.constraint_name = `unique_${data.tableName + column_name + addConstraintObjCopy.constraint_name = `unique_${ + data.tableName + column_name }`; // assign the constraint_type to 'UNIQUE' addConstraintObjCopy.constraint_type = 'UNIQUE'; @@ -348,6 +377,8 @@ function TableField({ data }: TableFieldProps) { return options; }; + // create a state for the foreign key drop down options + const [fkOptions, setFkOptions] = useState(createFieldOptions()); // disable the dropdown menus for fk table and field when fk checkbox is not checked const disableForeignKeyMenuHandler = (isChecked) => { @@ -378,11 +409,6 @@ function TableField({ data }: TableFieldProps) { 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); diff --git a/frontend/components/views/ERTables/TableFooterNode.tsx b/frontend/components/views/ERTables/TableFooterNode.tsx new file mode 100644 index 00000000..f46b21b1 --- /dev/null +++ b/frontend/components/views/ERTables/TableFooterNode.tsx @@ -0,0 +1,154 @@ +import React from 'react'; +import Tooltip from '@mui/material/Tooltip'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { Save } from '@mui/icons-material'; +// import { } from '@mui/icons-material'; +import IconButton from '@mui/material/IconButton'; +import TextField from '@mui/material/TextField'; +import { + AlterTablesObjType, + AddColumnsObjType, + DropTablesObjType, + TableHeaderDataObjectType, + AlterColumnsObjType, + BackendObjType, +} from '../../../types'; +import './styles.css'; +import * as colors from '../../../style-variables'; +import { sendFeedback } from '../../../lib/utils'; + +type TableFooterObjectType = { + table_name: string; + schemaStateCopy: any; + setSchemaState: (string) => {}; + backendObj: BackendObjType; +}; +type TableFooterProps = { + data; +}; + +function TableFooter({ data }: TableFooterProps) { + const { table_name, schemaStateCopy, setSchemaState, backendObj } = data; + // find table we are editing in schemaStateCopy to use throughout all of our TableHeader functions + const currentTable = schemaStateCopy.tableList.find( + (table) => table.table_name === table_name, + ); + // This function handles the add column button on the table + const handleAddColumn = (): void => { + // edit the schema table for this current table + // create an alterTableObject with AlterTablesObjectType + const alterTablesObj: AlterTablesObjType = { + is_insertable_into: null, + table_catalog: currentTable.table_catalog, + table_name, + new_table_name: null, + table_schema: currentTable.table_schema, + addColumns: [], + dropColumns: [], + alterColumns: [], + }; + // create an addColumnsType object + const addColumnsObj: AddColumnsObjType = { + column_name: `NewColumn${currentTable.columns?.length + 1 || 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 + currentTable.columns.push({ + column_name: `NewColumn${currentTable.columns?.length + 1 || 1}`, + new_column_name: `NewColumn${currentTable.columns?.length + 1 || 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); + }; + + // This function updates the table name when the user hits enter on the submit form + // const handleChangeTableName = (): void => { + // const tableInputField = document.getElementById( + // `table-name-form-${data.table_name}`, + // ) as HTMLInputElement; + + // console.log(tableInputField.value); + + // // update backend + // const alterColumnsArray: AlterColumnsObjType[] = []; + // for (let j = 0; j < currentTable.columns.length; j += 1) { + // const alterColumnsObj: AlterColumnsObjType = { + // column_name: currentTable.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 (currentTable.columns[j].constraint_type === 'PRIMARY KEY') { + // alterColumnsObj.rename_constraint = `pk_${currentTable.table_name}${currentTable.columns[j].column_name}`; + // alterColumnsArray.push(alterColumnsObj); + // } + // if (currentTable.columns[j].constraint_type === 'FOREIGN KEY') { + // alterColumnsObj.rename_constraint = `fk_${currentTable.table_name}${currentTable.columns[j].column_name}`; + // alterColumnsArray.push(alterColumnsObj); + // } + // if (currentTable.columns[j].constraint_type === 'UNIQUE') { + // alterColumnsObj.rename_constraint = `unique_${currentTable.table_name}${currentTable.columns[j].column_name}`; + // alterColumnsArray.push(alterColumnsObj); + // } + // } + // const alterTablesObj: AlterTablesObjType = { + // is_insertable_into: currentTable.is_insertable_into, + // table_catalog: currentTable.table_catalog, + // table_name: currentTable.table_name, + // new_table_name: tableInputField.value, + // table_schema: currentTable.table_schema, + // addColumns: [], + // dropColumns: [], + // alterColumns: alterColumnsArray, + // }; + + // // update frontend + // if (tableInputField !== null) { + // currentTable.new_table_name = tableInputField.value; + // setSchemaState(schemaStateCopy); + // console.log(schemaStateCopy); + // } + // console.log(alterTablesObj); + // backendObj.current.updates.alterTables.push(alterTablesObj); + // }; + + + return ( +
+ + + Add Column + + +
+ ); +} + +export default TableFooter; diff --git a/frontend/components/views/ERTables/TableHeaderNode.tsx b/frontend/components/views/ERTables/TableHeaderNode.tsx index df64bd28..c3956f2b 100644 --- a/frontend/components/views/ERTables/TableHeaderNode.tsx +++ b/frontend/components/views/ERTables/TableHeaderNode.tsx @@ -1,8 +1,10 @@ import React from 'react'; import Tooltip from '@mui/material/Tooltip'; import DeleteIcon from '@mui/icons-material/Delete'; +import { Save } from '@mui/icons-material'; import IconButton from '@mui/material/IconButton'; import TextField from '@mui/material/TextField'; +import { InputAdornment } from '@mui/material'; import { AlterTablesObjType, AddColumnsObjType, @@ -24,45 +26,6 @@ function TableHeader({ data }: TableHeaderProps) { const currentTable = schemaStateCopy.tableList.find( (table) => table.table_name === table_name, ); - // This function handles the add column button on the table - const handleAddColumn = (): void => { - // edit the schema table for this current table - // create an alterTableObject with AlterTablesObjectType - const alterTablesObj: AlterTablesObjType = { - is_insertable_into: null, - table_catalog: currentTable.table_catalog, - table_name, - new_table_name: null, - table_schema: currentTable.table_schema, - addColumns: [], - dropColumns: [], - alterColumns: [], - }; - // create an addColumnsType object - const addColumnsObj: AddColumnsObjType = { - column_name: `NewColumn${currentTable.columns?.length + 1 || 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 - currentTable.columns.push({ - column_name: `NewColumn${currentTable.columns?.length + 1 || 1}`, - new_column_name: `NewColumn${currentTable.columns?.length + 1 || 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); - }; // This function handles the add delete button on the table const handleDeleteTable = (): void => { @@ -85,64 +48,64 @@ function TableHeader({ data }: TableHeaderProps) { sendFeedback({ type: 'error', message: - 'WARNING: Changing table name will only rename constraints in fk_tableNameColumnName format. Use at your own discretion.', + 'WARNING: Table name saved, but changing table name will only rename constraints in fk_tableNameColumnName format. Use at your own discretion.', }); }; - // This function updates the table name when the user hits enter on the submit form - const handleChangeTableName = (e): void => { - if (e.key === 'Enter') { - const tableInputField = document.getElementById( - `table-name-form-${data.table_name}`, - ) as HTMLInputElement; - // update backend - const alterColumnsArray: AlterColumnsObjType[] = []; - for (let j = 0; j < currentTable.columns.length; j += 1) { - const alterColumnsObj: AlterColumnsObjType = { - column_name: currentTable.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 (currentTable.columns[j].constraint_type === 'PRIMARY KEY') { - alterColumnsObj.rename_constraint = `pk_${currentTable.table_name}${currentTable.columns[j].column_name}`; - alterColumnsArray.push(alterColumnsObj); - } - if (currentTable.columns[j].constraint_type === 'FOREIGN KEY') { - alterColumnsObj.rename_constraint = `fk_${currentTable.table_name}${currentTable.columns[j].column_name}`; - alterColumnsArray.push(alterColumnsObj); - } - if (currentTable.columns[j].constraint_type === 'UNIQUE') { - alterColumnsObj.rename_constraint = `unique_${currentTable.table_name}${currentTable.columns[j].column_name}`; - alterColumnsArray.push(alterColumnsObj); - } - } - const alterTablesObj: AlterTablesObjType = { - is_insertable_into: currentTable.is_insertable_into, - table_catalog: currentTable.table_catalog, - table_name: currentTable.table_name, - new_table_name: tableInputField.value, - table_schema: currentTable.table_schema, - addColumns: [], - dropColumns: [], - alterColumns: alterColumnsArray, - }; + const handleChangeTableName = (): void => { + const tableInputField = document.getElementById( + `table-name-form-${data.table_name}`, + ) as HTMLInputElement; - // update frontend - if (tableInputField !== null) { - currentTable.new_table_name = tableInputField.value; - setSchemaState(schemaStateCopy); + // update backend + const alterColumnsArray: AlterColumnsObjType[] = []; + for (let j = 0; j < currentTable.columns.length; j += 1) { + const alterColumnsObj: AlterColumnsObjType = { + column_name: currentTable.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 (currentTable.columns[j].constraint_type === 'PRIMARY KEY') { + alterColumnsObj.rename_constraint = `pk_${currentTable.table_name}${currentTable.columns[j].column_name}`; + alterColumnsArray.push(alterColumnsObj); + } + if (currentTable.columns[j].constraint_type === 'FOREIGN KEY') { + alterColumnsObj.rename_constraint = `fk_${currentTable.table_name}${currentTable.columns[j].column_name}`; + alterColumnsArray.push(alterColumnsObj); + } + if (currentTable.columns[j].constraint_type === 'UNIQUE') { + alterColumnsObj.rename_constraint = `unique_${currentTable.table_name}${currentTable.columns[j].column_name}`; + alterColumnsArray.push(alterColumnsObj); } - backendObj.current.updates.alterTables.push(alterTablesObj); } + const alterTablesObj: AlterTablesObjType = { + is_insertable_into: currentTable.is_insertable_into, + table_catalog: currentTable.table_catalog, + table_name: currentTable.table_name, + new_table_name: tableInputField.value, + table_schema: currentTable.table_schema, + addColumns: [], + dropColumns: [], + alterColumns: alterColumnsArray, + }; + + // update frontend + if (tableInputField !== null) { + currentTable.new_table_name = tableInputField.value; + setSchemaState(schemaStateCopy); + console.log(schemaStateCopy); + } + console.log(alterTablesObj); + backendObj.current.updates.alterTables.push(alterTablesObj); }; return ( @@ -150,26 +113,27 @@ function TableHeader({ data }: TableHeaderProps) { style={{ backgroundColor: colors.greyLightest }} className="table-header table" > - - - - - - Add Column - - + + + + + + ), + }} + id={`table-name-form-${data.table_name}`} + label="Table Name" + variant="outlined" + defaultValue={data.table_name} + // onKeyDown={handleChangeTableName} + onClick={warnUser} + style={{ backgroundColor: 'white' }} + /> diff --git a/frontend/components/views/QueryView/QueryHistory.tsx b/frontend/components/views/QueryView/QueryHistory.tsx new file mode 100644 index 00000000..155c14fe --- /dev/null +++ b/frontend/components/views/QueryView/QueryHistory.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { useState } from 'react'; +import { Select, MenuItem, InputLabel, FormControl } from '@mui/material/'; +import Box from '@mui/material/Box'; +import styled from 'styled-components'; +import { defaultMargin, greyPrimary } from '../../../style-variables'; +import { SelectChangeEvent } from '@mui/material/Select'; + +const SpacedBox = styled(Box)` + margin-left: ${defaultMargin}; + margin-top: 12px; + display: flex; + align-items: center; + justify-content: center; +`; + +const StyledMenuItem = styled(MenuItem)` + color: #575151; + outline: none; +`; + +const StyledFormControl = styled(FormControl)` + margin-top: 15px; +`; + +const StyledSelect = styled(Select)` + outline: none; + display: flex; + justify-content: center; + border-top: 1px solid black; + border-right: 1px solid black; + border-bottom: 1px solid black; + border-radius: none; +`; + +const StyledInputLabel = styled(InputLabel)` + border-top: 1px solid black; + border-left: 1px solid black; + border-bottom: 1px solid black; + padding-top: 16.5px; + padding-bottom: 16.5px; + padding-left: 10px; + border-radius: 4px; +`; + +interface QueryHistoryProps { + history: string[]; + onChange: (newSql: string) => void; +} + +function QueryHistory({ history, onChange }: QueryHistoryProps) { + console.log('history', history) + const label = 'Previous Queries'; + const noDups = [...new Set(history)]; + + console.log('NODUPS', noDups); + const handleSelection = (e: SelectChangeEvent) => { + console.log(e.target.value); + onChange(e.target.value as string); + }; + return ( +
+ + {label} + + +
+ ); +} + +export default QueryHistory; diff --git a/frontend/components/views/QueryView/QuerySqlInput.tsx b/frontend/components/views/QueryView/QuerySqlInput.tsx index 129f6c1a..48ab1bd2 100644 --- a/frontend/components/views/QueryView/QuerySqlInput.tsx +++ b/frontend/components/views/QueryView/QuerySqlInput.tsx @@ -1,11 +1,13 @@ /* eslint-disable import/no-extraneous-dependencies */ -import React from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import FormatPaintIcon from '@mui/icons-material/FormatPaint'; import { ButtonGroup, Button, Tooltip } from '@mui/material'; import styled from 'styled-components'; import { formatDialect, postgresql } from 'sql-formatter'; -import CodeMirror from '@uiw/react-codemirror'; -import { dracula } from '@uiw/codemirror-theme-dracula'; +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution'; +import QueryHistory from './QueryHistory'; +import { exists } from 'fs'; const Container = styled.div` position: relative; @@ -27,20 +29,83 @@ const Toolbar = styled.div` opacity: 1; } `; +monaco.editor.defineTheme('sql-theme', { + base: 'vs-dark', + inherit: true, + colors: { + 'editor.background': '#1e1e1e', + 'editor.foreground': '#d4d4d4', + }, + rules: [{ token: 'sql-keyword', foreground: '#828562', fontStyle: 'bold' }], +}); + +monaco.editor.setTheme('sql-theme'); interface QuerySqlInputProps { sql: string; onChange: (newSql: string) => void; - runQuery: () => void; } -function QuerySqlInput({ sql, onChange, runQuery }: QuerySqlInputProps) { +function QuerySqlInput({ sql, onChange }: QuerySqlInputProps) { + const editorRef = useRef(null); + + useEffect(() => { + const container = document.getElementById('editor-container'); + if(editorRef.current) { + // model will just be an obj + const model = editorRef.current.getModel(); + // get the value in sql + const currentSql = model?.getValue(); + // checks if the currentSql is not sql then we will reassign the model value to sql + if(currentSql !== sql) { + model?.setValue(sql) + } + } + + const initializeEditor = () => { + // console.log('sql', sql) + if (container && !editorRef.current) { + editorRef.current = monaco.editor.create(container, { + value: sql, + language: 'sql', + theme: 'sql-theme', + }); + + editorRef.current.onDidChangeModelContent(() => { + const newSql = editorRef.current?.getValue(); + onChange(newSql || ''); + }); + } + }; + + const loaderScript = document.createElement('script'); + loaderScript.src = + 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.22.3/min/vs/loader.min.js'; + loaderScript.async = true; + loaderScript.onload = initializeEditor; + + document.body.appendChild(loaderScript); + }); + const formatQuery = () => { - const formatted = formatDialect(sql, { - dialect: postgresql, - keywordCase: 'upper', - }); - onChange(formatted); + if (editorRef.current) { + const model = editorRef.current.getModel(); + console.log(model); + if (model) { + // Get the current content of the editor + const content = model.getValue(); + // Format the SQL query using the formatting provider + const formatted = formatDialect(content, { + dialect: postgresql, + keywordCase: 'upper', + }); + // Apply the formatted content back to the editor + monaco.editor.setModelLanguage(model, 'sql'); + model.setValue(formatted); + // Update the parent component's state with the formatted query + onChange(formatted); + } + } }; return ( @@ -54,13 +119,11 @@ function QuerySqlInput({ sql, onChange, runQuery }: QuerySqlInputProps) {
- diff --git a/frontend/components/views/QueryView/QueryView.tsx b/frontend/components/views/QueryView/QueryView.tsx index 3b21495d..7f8d1760 100644 --- a/frontend/components/views/QueryView/QueryView.tsx +++ b/frontend/components/views/QueryView/QueryView.tsx @@ -16,6 +16,7 @@ import QuerySqlInput from './QuerySqlInput'; import QuerySummary from './QuerySummary'; import QueryTabs from './QueryTabs'; import QueryRunNumber from './QueryRunNumber'; +import QueryHistory from './QueryHistory'; import { useQueryContext, @@ -81,10 +82,15 @@ function QueryView({ averageSampleTime: 0, }; + const localQuery = { ...defaultQuery, ...queryStateContext?.workingQuery }; const [runQueryNumber, setRunQueryNumber] = useState(1); + const [queriesRan, setQueriesRan] = useState([]); + + const [selectedQueryHx, setSelectedQueryHx] = useState(''); + const onLabelChange = (newLabel: string) => { queryDispatchContext!({ type: 'UPDATE_WORKING_QUERIES', @@ -103,7 +109,7 @@ function QueryView({ // when db is changed we must change selected db state on app, as well as // request updates for db and table information. Otherwise database view tab // will show wrong information - + setSelectedDb(newDb); setDBType(nextDBType); @@ -124,7 +130,7 @@ function QueryView({ ); }; const onSqlChange = (newSql: string) => { - // because App's workingQuery changes ref + console.log(newSql); queryDispatchContext!({ type: 'UPDATE_WORKING_QUERIES', payload: { ...localQuery, sqlString: newSql }, @@ -132,6 +138,7 @@ function QueryView({ }; const onRun = () => { + console.log(localQuery.sqlString); if (!localQuery.label.trim()) { sendFeedback({ type: 'info', @@ -171,6 +178,14 @@ function QueryView({ maximumSampleTime, averageSampleTime, }) => { + if (returnedRows) { + if (queriesRan.length === 5) { + queriesRan.pop(); + } + queriesRan.unshift(sqlString); + // setReturnedRows(returnedRows.length) + setQueriesRan(queriesRan); + } if (error) { throw error; } @@ -276,7 +291,7 @@ function QueryView({ if (!show) return null; return ( - + + , document.getElementById('root'), ); + +// const container = document.getElementById('root') as HTMLElement; +// const root = createRoot(container); +// root.render(); diff --git a/frontend/lib/convertStateToReactFlow.ts b/frontend/lib/convertStateToReactFlow.ts index 75a45209..69905f95 100644 --- a/frontend/lib/convertStateToReactFlow.ts +++ b/frontend/lib/convertStateToReactFlow.ts @@ -19,7 +19,7 @@ interface TableConstructor { id: number, columns: ERTableColumnData[], name: string, - tableCoordinates: { x: number; y: number }, + table_position: { x: number; y: number }, otherTables: { table_name: string; column_names: string[]; @@ -41,7 +41,7 @@ const Table: TableConstructor = class Table implements TableInterface { private id: number; private columns: ERTableColumnData[]; private name: string; - private tableCoordinates: { x: number; y: number }; + private table_position: { x: number; y: number }; private otherTables: { table_name: string; column_names: string[]; @@ -52,7 +52,7 @@ const Table: TableConstructor = class Table implements TableInterface { id: number, columns: ERTableColumnData[], name: string, - tableCoordinates: { x: number; y: number }, + table_position: { x: number; y: number }, otherTables: { table_name: string; column_names: string[]; @@ -62,37 +62,31 @@ const Table: TableConstructor = class Table implements TableInterface { this.id = id; this.columns = columns; this.name = name; - this.tableCoordinates = tableCoordinates; + this.table_position = table_position; this.otherTables = otherTables; this.database = database; } + // the render method converts the data into the form of react flow render() { - // This method gets the table position from the stored file + // gets dbname and table positions from the localStorage file + const layoutFlowKey = 'layout-key'; + const existingLayouts = JSON.parse(localStorage.getItem(layoutFlowKey) ?? '[]'); + const getTablePosition = (): { x: number; y: number } => { - try { - const location = app.getPath('temp').concat('/UserTableLayouts.json'); - // refactored code. parse json file, look for current db in saved file, look for current table inside db. return undefined if db or table doesn't exist - const parsedData: unknown = JSON.parse( - fs.readFileSync(location, 'utf8'), - ); - const foundCurrentDB = isDatabaseLayoutObjTypeArr(parsedData) - ? parsedData.find( - (db: DatabaseLayoutObjType) => db?.db_name === this.database, - ) - : undefined; - const foundCurrentTable = foundCurrentDB?.db_tables.find( - (table) => table.table_name === this.name, - ); - // return current table's saved position coordinates else return passed in coordinates if could not find saved coordinates in json - return foundCurrentTable - ? foundCurrentTable.table_position - : { x: this.tableCoordinates.x, y: this.tableCoordinates.y }; - } catch (error) { - return { x: (this.id - 1) * 500, y: 0 }; - } + const savedTable = existingLayouts.find( + (layout) => layout.db_name === this.database + )?.db_tables.find((table) => table.id === `table-${this.name}`); + + return savedTable + ? savedTable.table_position + : { x: (this.id - 1) * 500, y: 0 }; }; + + ; + // const test = getTablePosition(); + // console.log(test); // create a nodes array for react flow, the first element will always be a // TABLE_HEADER type of node const nodes: Node[] = [ @@ -105,6 +99,8 @@ const Table: TableConstructor = class Table implements TableInterface { }, }, ]; + + const edges: Edge[] = []; let num = -1; @@ -125,6 +121,7 @@ const Table: TableConstructor = class Table implements TableInterface { type: types.TABLE_FIELD, parentNode: `table-${this.name}`, draggable: false, + // position: { x: getTablePosition().x, y: (num + 1) * 78 }, position: { x: 0, y: (num + 1) * 78 }, data: { tableName: this.name, @@ -148,6 +145,43 @@ const Table: TableConstructor = class Table implements TableInterface { }); } }); + + const filterByField = nodes.filter((ele) => ele.type === types.TABLE_FIELD); + const grabLastFieldPosition = + filterByField[filterByField.length - 1]?.position?.y; + // console.log(grabLastFieldPosition); + + const test = grabLastFieldPosition; + // console.log(getTablePosition()); + + if (test) { + // console.log(getTablePosition()); + nodes.push({ + id: `table-footer-${this.name}`, + type: types.TABLE_FOOTER, + position: { + x: 0, + y: grabLastFieldPosition + 78, + }, + data: { + table_name: this.name, + }, + parentNode: `table-${this.name}`, + draggable: false, + }); + } else { + nodes.push({ + id: `table-footer-${this.name}`, + type: types.TABLE_FOOTER, + position: { x: 0, y: 78 }, + data: { + table_name: this.name, + }, + parentNode: `table-${this.name}`, + draggable: false, + }); + } + // return an object with nodes and edges return { nodes, @@ -191,23 +225,26 @@ const convertStateToReactFlow = { ); // if current table has more columns than any other in its row, set columnGap to new max number of columns * 74(px) // filter for duplicate column names -- one imported test db was creating a new column for each constraint and this is a bandaid fix - const columnsGapSet = new Set(); - tables[i].columns.forEach((column) => - columnsGapSet.add(column.column_name), - ); - columnGap = Math.max(columnsGapSet.size * 74, columnGap); - // calculate a default rowLength based on sqrt of number of tables - const rowLength = Math.floor(Math.sqrt(tables.length)); - // if table should be the beginning of a new row... - if (i % rowLength === 0) { - // set x, y coordinates for new row to 0 and +250 respectively; - tableCoordinates.x = 0; - tableCoordinates.y += 250 + columnGap; - columnGap = 0; - } else { - // ...otherwise increment tables position horizontally in current row. - tableCoordinates.x += 500; - } + // const columnsGapSet = new Set(); + // tables[i].columns.forEach((column) => + // columnsGapSet.add(column.column_name), + // ); + // console.log(columnsGapSet); + + // columnGap = Math.max(columnsGapSet.size * 74, columnGap); + // console.log(columnGap); + // // calculate a default rowLength based on sqrt of number of tables + // const rowLength = Math.floor(Math.sqrt(tables.length)); + // // if table should be the beginning of a new row... + // if (i % rowLength === 0) { + // // set x, y coordinates for new row to 0 and +250 respectively; + // tableCoordinates.x = 0; + // tableCoordinates.y += 250 + columnGap; + // columnGap = 0; + // } else { + // // ...otherwise increment tables position horizontally in current row. + // tableCoordinates.x += 500; + // } // create a new instance of Table, push into table array const table = new Table( i + 1, diff --git a/frontend/lib/queries.ts b/frontend/lib/queries.ts index bd512209..c46b7bca 100644 --- a/frontend/lib/queries.ts +++ b/frontend/lib/queries.ts @@ -78,7 +78,6 @@ export const createNewQuery: CreateNewQuery = (query: QueryData, queries) => { // Only save query to saved queries if it contains all minimum information if (query?.label && query?.db && query?.sqlString && query?.group) { const newQueries = createQuery(queries, query); - return newQueries; } return []; diff --git a/frontend/lib/style.css b/frontend/lib/style.css index b850e781..31c20dd0 100644 --- a/frontend/lib/style.css +++ b/frontend/lib/style.css @@ -79,6 +79,97 @@ main { margin-top: 1rem; } +.hide-3d-btn { + color: rgba(255, 255, 255, .8); + border: 2px solid rgba(255, 255, 255, .8); + border-radius: .5rem; + background: transparent; + padding: 1rem .5rem; + cursor: pointer; + outline: none; +} + +:root { + --greyPrimary: #F0386B; +} + +main { + min-width: 650px; +} + /* greyPrimary = '#818584'; */ + + /* button { + /* background: #11998E; /* fallback for old browsers */ + /* background: -webkit-linear-gradient(#AFCEB9, #097548); /* Chrome 10-25, Safari 5.1-6 */ + /* background: linear-gradient(#AFCEB9, #097548); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ + /* }; */ + +.tabs { + background-color: var(--greyPrimary); + color: white; + border-radius: 5px; +}; + +.db-login-tab.Mui-selected { + color: rgb(25, 118, 210) !important; +} + +.db-login-tabs .MuiTabs-indicator { + background-color: rgb(25, 118, 210); +} + +/* QuickStartView */ +.step-btn { + margin-right: 8px; +} + +.step-btn { + margin-right: 8px; +} + +.step-back-btn { + margin-right: 8px; +} + +.step-completed { + display: inline-block; +} + +.step-instructions { + margin-bottom: 24px; +} + +.step-img { + margin-top: 16px; + margin-bottom: -32px; + width: 20vh; + height: 20vh; + max-height: 300px; + max-width: 300px; +} + +.stepper { + font-size: 50px; +} + +.Mui-selected:not(.db-login-tab):not(.tables-view-btn) { + color: rgb(255, 255, 255) !important; + background-color: #11774e; + opacity: 1 !important; +} + +.MuiTabs-scroller > div > button:not(.db-login-tab) { + color: rgba(255, 255, 255, .3); + font-size: 1.25rem; + border-right: 1px solid rgba(255, 255, 255, .1); + border-left: 1px solid rgba(255, 255, 255, .1); + width: 33%; +} + +.query-run-box { + margin-top: 1rem; +} + .hide-3d-btn { color: rgba(255, 255, 255, .8); border: 2px solid rgba(255, 255, 255, .8); diff --git a/frontend/state_management/Reducers/MenuReducers.ts b/frontend/state_management/Reducers/MenuReducers.ts index 6551e4e3..a645aca7 100644 --- a/frontend/state_management/Reducers/MenuReducers.ts +++ b/frontend/state_management/Reducers/MenuReducers.ts @@ -100,7 +100,7 @@ function menuReducer(state: MenuState, action: MenuActions): MenuState { export const submitAsyncToBackend = ( issued: number, asyncList: Map, - invoke: (e: string, p: any) => Promise, + invoke: (e: string, p: string | number) => Promise, menuDispatch: (v: MenuActions) => void, ) => { const request = asyncList.get(issued); diff --git a/frontend/types.ts b/frontend/types.ts index ab97a979..29b05f46 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -246,11 +246,15 @@ export interface ExplainJson { * */ -export type ERTablingConstants = { TABLE_HEADER } | { TABLE_FIELD }; +export type ERTablingConstants = + | { TABLE_HEADER } + | { TABLE_FIELD } + | { TABLE_FOOTER }; export type NodeTypes = { tableHeader: JSX.Element; tableField: JSX.Element; + tableFooter: JSX.Element; }; export interface ERTableData extends TableInfo { diff --git a/jest.config.ts b/jest.config.ts index 3d138c03..ee8abc91 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -137,7 +137,7 @@ const config: Config = { // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + setupFilesAfterEnv: ["./shared/jest.setup.ts"], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, @@ -197,3 +197,4 @@ const config: Config = { }; export default config; + diff --git a/package-lock.json b/package-lock.json index 384377ba..ec5dd870 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,15 +10,15 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@codemirror/basic-setup": "^0.20.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@faker-js/faker": "^8.0.2", - "@mui/icons-material": "^5.14.3", + "@mui/icons-material": "^5.15.6", "@mui/material": "^5.14.5", "@uiw/codemirror-theme-dracula": "^4.21.9", "@uiw/react-codemirror": "^4.21.9", "chart.js": "^4.3.3", - "codemirror": "^6.0.1", "concurrently": "^8.2.1", "cross-env": "^7.0.3", "d3": "^7.8.5", @@ -29,7 +29,9 @@ "electron-store": "^8.1.0", "fix-path": "^3.0.0", "fontsource-roboto": "^4.0.0", + "monaco-editor": "^0.45.0", "ms": "^2.1.3", + "mysql": "^2.18.1", "mysql2": "^3.6.0", "pg": "^8.11.3", "react": "^18.2.0", @@ -40,6 +42,7 @@ "sql-formatter": "^12.2.4", "sqlite3": "^5.1.6", "styled-components": "^6.0.7", + "text-encoding": "^0.7.0", "three": "^0.155.0", "three-spritetext": "^1.8.1" }, @@ -49,6 +52,7 @@ "@babel/preset-react": "^7.22.5", "@babel/preset-typescript": "^7.22.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@testing-library/jest-dom": "^6.4.1", "@testing-library/react": "^14.0.0", "@types/chart.js": "^2.9.37", "@types/debounce": "^1.2.1", @@ -68,7 +72,7 @@ "@typescript-eslint/parser": "^6.4.1", "babel-loader": "^9.1.3", "css-loader": "^6.8.1", - "electron": "^25.5.0", + "electron": "^25.9.8", "electron-builder": "^24.6.3", "enzyme": "^3.11.0", "eslint": "^8.47.0", @@ -112,6 +116,12 @@ "node": ">=0.10.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "dev": true + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -1866,11 +1876,11 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.6.tgz", - "integrity": "sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==", + "version": "7.23.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz", + "integrity": "sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -1945,6 +1955,112 @@ "@lezer/common": "^1.0.0" } }, + "node_modules/@codemirror/basic-setup": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/basic-setup/-/basic-setup-0.20.0.tgz", + "integrity": "sha512-W/ERKMLErWkrVLyP5I8Yh8PXl4r+WFNkdYVSzkXYPQv2RMPSkWpr2BgggiSJ8AHF/q3GuApncDD8I4BZz65fyg==", + "deprecated": "In version 6.0, this package has been renamed to just 'codemirror'", + "dependencies": { + "@codemirror/autocomplete": "^0.20.0", + "@codemirror/commands": "^0.20.0", + "@codemirror/language": "^0.20.0", + "@codemirror/lint": "^0.20.0", + "@codemirror/search": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/autocomplete": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-0.20.3.tgz", + "integrity": "sha512-lYB+NPGP+LEzAudkWhLfMxhTrxtLILGl938w+RcFrGdrIc54A+UgmCoz+McE3IYRFp4xyQcL4uFJwo+93YdgHw==", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/commands": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-0.20.0.tgz", + "integrity": "sha512-v9L5NNVA+A9R6zaFvaTbxs30kc69F6BkOoiEbeFw4m4I0exmDEKBILN6mK+GksJtvTzGBxvhAPlVFTdQW8GB7Q==", + "dependencies": { + "@codemirror/language": "^0.20.0", + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/language": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-0.20.2.tgz", + "integrity": "sha512-WB3Bnuusw0xhVvhBocieYKwJm04SOk5bPoOEYksVHKHcGHFOaYaw+eZVxR4gIqMMcGzOIUil0FsCmFk8yrhHpw==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "@lezer/common": "^0.16.0", + "@lezer/highlight": "^0.16.0", + "@lezer/lr": "^0.16.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/lint": { + "version": "0.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-0.20.3.tgz", + "integrity": "sha512-06xUScbbspZ8mKoODQCEx6hz1bjaq9m8W8DxdycWARMiiX1wMtfCh/MoHpaL7ws/KUMwlsFFfp2qhm32oaCvVA==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.2", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/search": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-0.20.1.tgz", + "integrity": "sha512-ROe6gRboQU5E4z6GAkNa2kxhXqsGNbeLEisbvzbOeB7nuDYXUZ70vGIgmqPu0tB+1M3F9yWk6W8k2vrFpJaD4Q==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "@codemirror/view": "^0.20.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/state": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-0.20.1.tgz", + "integrity": "sha512-ms0tlV5A02OK0pFvTtSUGMLkoarzh1F8mr6jy1cD7ucSC2X/VLHtQCxfhdSEGqTYlQF2hoZtmLv+amqhdgbwjQ==" + }, + "node_modules/@codemirror/basic-setup/node_modules/@codemirror/view": { + "version": "0.20.7", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-0.20.7.tgz", + "integrity": "sha512-pqEPCb9QFTOtHgAH5XU/oVy9UR/Anj6r+tG5CRmkNVcqSKEPmBU05WtN/jxJCFZBXf6HumzWC9ydE4qstO3TxQ==", + "dependencies": { + "@codemirror/state": "^0.20.0", + "style-mod": "^4.0.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/common": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.16.1.tgz", + "integrity": "sha512-qPmG7YTZ6lATyTOAWf8vXE+iRrt1NJd4cm2nJHK+v7X9TsOF6+HtuU/ctaZy2RCrluxDb89hI6KWQ5LfQGQWuA==" + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/highlight": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-0.16.0.tgz", + "integrity": "sha512-iE5f4flHlJ1g1clOStvXNLbORJoiW4Kytso6ubfYzHnaNo/eo5SKhxs4wv/rtvwZQeZrK3we8S9SyA7OGOoRKQ==", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, + "node_modules/@codemirror/basic-setup/node_modules/@lezer/lr": { + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-0.16.3.tgz", + "integrity": "sha512-pau7um4eAw94BEuuShUIeQDTf3k4Wt6oIUOYxMmkZgDHdqtIcxWND4LRxi8nI9KuT4I1bXQv67BCapkxt7Ywqw==", + "dependencies": { + "@lezer/common": "^0.16.0" + } + }, "node_modules/@codemirror/commands": { "version": "6.2.4", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.4.tgz", @@ -3582,18 +3698,18 @@ } }, "node_modules/@mui/icons-material": { - "version": "5.14.3", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.3.tgz", - "integrity": "sha512-XkxWPhageu1OPUm2LWjo5XqeQ0t2xfGe8EiLkRW9oz2LHMMZmijvCxulhgquUVTF1DnoSh+3KoDLSsoAFtVNVw==", + "version": "5.15.6", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.6.tgz", + "integrity": "sha512-GnkxMtlhs+8ieHLmCytg00ew0vMOiXGFCw8Ra9nxMsBjBqnrOI5gmXqUm+sGggeEU/HG8HyeqC1MX/IxOBJHzA==", "dependencies": { - "@babel/runtime": "^7.22.6" + "@babel/runtime": "^7.23.8" }, "engines": { "node": ">=12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/mui" + "url": "https://opencollective.com/mui-org" }, "peerDependencies": { "@mui/material": "^5.0.0", @@ -4196,6 +4312,118 @@ "node": ">=8" } }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.1.tgz", + "integrity": "sha512-Z7qMM3J2Zw5H/nC2/5CYx5YcuaD56JmDFKNIozZ89VIo6o6Y9FMhssics4e2madEKYDNEpZz3+glPGz0yWMOag==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.2", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.15", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@testing-library/react": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.0.0.tgz", @@ -6791,23 +7019,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-styled-components": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", - "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", - "optional": true, - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "lodash": "^4.17.21", - "picomatch": "^2.3.1" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", @@ -6895,6 +7106,14 @@ "node": "*" } }, + "node_modules/bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -8121,8 +8340,7 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/cosmiconfig": { "version": "7.1.0", @@ -8318,6 +8536,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -9438,9 +9662,9 @@ } }, "node_modules/electron": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-25.6.0.tgz", - "integrity": "sha512-MYScf2Pwc2IzpIQ7+IpmxSrNb38B4nra67J+XN41gX1I5PNvD7iJEQ5qQ4qlEu+aPvRU+8fDaB02JteEYeAyzA==", + "version": "25.9.8", + "resolved": "https://registry.npmjs.org/electron/-/electron-25.9.8.tgz", + "integrity": "sha512-PGgp6PH46QVENHuAHc2NT1Su8Q1qov7qIl2jI5tsDpTibwV2zD8539AeWBQySeBU4dhbj9onIl7+1bXQ0wefBg==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -12833,7 +13057,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "optional": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -16186,6 +16410,15 @@ "dom-walk": "^0.1.0" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -16329,6 +16562,11 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.45.0.tgz", + "integrity": "sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==" + }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -16361,6 +16599,60 @@ "multicast-dns": "cli.js" } }, + "node_modules/mysql": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", + "integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==", + "dependencies": { + "bignumber.js": "9.0.0", + "readable-stream": "2.3.7", + "safe-buffer": "5.1.2", + "sqlstring": "2.3.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/mysql/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/mysql/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/mysql/node_modules/sqlstring": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", + "integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mysql/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/mysql2": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.6.0.tgz", @@ -21229,8 +21521,7 @@ "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "node_modules/progress": { "version": "2.0.3", @@ -21824,6 +22115,19 @@ "node": ">= 10.13.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.3.tgz", @@ -21861,9 +22165,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -23386,6 +23690,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -23751,6 +24067,12 @@ "node": ">=8" } }, + "node_modules/text-encoding": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", + "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", + "deprecated": "no longer maintained" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -24338,20 +24660,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/package.json b/package.json index 73eaad58..65336085 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "seeqr", "productName": "SeeQR App", - "version": "12.0.0", + "version": "13.0.0", "description": "SQL Toolbox - Model Implementer, Execution Plan Visualizer and Query Comparison Tool", "main": "./tsCompiled/backend/main.js", "babel": { @@ -19,7 +19,7 @@ "electron-dev": "nodemon --watch tsCompiled/backend --exec \"npx cross-env NODE_ENV=development electron --no-sandbox .\"", "tsc-dev": "tsc --watch", "test": "jest --verbose", - "electron-build": "npm run build && electron-builder" + "electron-build": "npm run build && electron-builder --linux" }, "repository": { "type": "git", @@ -32,15 +32,15 @@ }, "license": "MIT", "dependencies": { + "@codemirror/basic-setup": "^0.20.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@faker-js/faker": "^8.0.2", - "@mui/icons-material": "^5.14.3", + "@mui/icons-material": "^5.15.6", "@mui/material": "^5.14.5", "@uiw/codemirror-theme-dracula": "^4.21.9", "@uiw/react-codemirror": "^4.21.9", "chart.js": "^4.3.3", - "codemirror": "^6.0.1", "concurrently": "^8.2.1", "cross-env": "^7.0.3", "d3": "^7.8.5", @@ -51,7 +51,9 @@ "electron-store": "^8.1.0", "fix-path": "^3.0.0", "fontsource-roboto": "^4.0.0", + "monaco-editor": "^0.45.0", "ms": "^2.1.3", + "mysql": "^2.18.1", "mysql2": "^3.6.0", "pg": "^8.11.3", "react": "^18.2.0", @@ -62,6 +64,7 @@ "sql-formatter": "^12.2.4", "sqlite3": "^5.1.6", "styled-components": "^6.0.7", + "text-encoding": "^0.7.0", "three": "^0.155.0", "three-spritetext": "^1.8.1" }, @@ -71,6 +74,7 @@ "@babel/preset-react": "^7.22.5", "@babel/preset-typescript": "^7.22.5", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@testing-library/jest-dom": "^6.4.1", "@testing-library/react": "^14.0.0", "@types/chart.js": "^2.9.37", "@types/debounce": "^1.2.1", @@ -90,7 +94,7 @@ "@typescript-eslint/parser": "^6.4.1", "babel-loader": "^9.1.3", "css-loader": "^6.8.1", - "electron": "^25.5.0", + "electron": "^25.9.8", "electron-builder": "^24.6.3", "enzyme": "^3.11.0", "eslint": "^8.47.0", diff --git a/roadmap.md b/roadmap.md index 0a6b7188..71342b61 100644 --- a/roadmap.md +++ b/roadmap.md @@ -2,10 +2,12 @@ ## view previous versions roadmap commits for older improvement goals -### v11 roadmap for future iterators +### v13 roadmap for future iterators - General + - Refactor codebase to elimiate prop-drilling allowing for future iterability. + - Update ui of the initial landing page of application with cloud database instructions - continue working on state management (usereducer, usecontext, etc) diff --git a/shared/jest.setup.ts b/shared/jest.setup.ts new file mode 100644 index 00000000..e6cfa5bc --- /dev/null +++ b/shared/jest.setup.ts @@ -0,0 +1,3 @@ +import { TextEncoder } from 'util'; + +global.TextEncoder = TextEncoder; diff --git a/tsconfig.json b/tsconfig.json index 869d1ec4..3b7e2993 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -79,5 +79,5 @@ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, "resolveJsonModule": true /* Include modules imported with '.json' extension. Requires TypeScript version 2.9 or later. */ }, - "include": ["backend", "frontend", "__tests__", "shared"] + "include": ["backend", "frontend", "__tests__", "shared", "./node_modules/@monaco-editor/react"] }