├── .DS_Store ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── build ├── bundle.js └── index.html ├── client ├── App.jsx ├── actions │ └── actions.js ├── components │ ├── AlertBar.jsx │ ├── AlertBox.jsx │ ├── AlertsHeader.jsx │ ├── DashAlertBarChart.jsx │ ├── DashAlertLineChart.jsx │ ├── DashTableOfTables.jsx │ ├── DashboardHeader.jsx │ ├── ErdVisualizer.jsx │ ├── Focus.jsx │ ├── FocusBar.jsx │ ├── Header.jsx │ ├── LandingVisualizer.jsx │ ├── Login.jsx │ ├── MonitorHeader.jsx │ ├── NodeStyle.jsx │ ├── QueryHeader.jsx │ ├── SideBar.jsx │ ├── SubHeader.jsx │ ├── monitors │ │ ├── CustomMonitor.jsx │ │ ├── FreshnessMonitor.jsx │ │ ├── MonitorEditor.jsx │ │ ├── NullMonitor.jsx │ │ ├── RangeMonitor.jsx │ │ ├── VolumeMonitor.jsx │ │ └── monitorObjectCreator.js │ └── stylesheets │ │ ├── App.css │ │ ├── Themes.jsx │ │ └── visualizer.css ├── constants │ └── actionTypes.js ├── containers │ ├── AlertContainer.jsx │ ├── AppContainer.jsx │ ├── DashboardContainer.jsx │ ├── ErdVisualizerContainer.jsx │ ├── LandingContainer.jsx │ ├── MainContainer.jsx │ ├── MonitorContainer.jsx │ ├── PageContainer.jsx │ ├── QueryContainer.jsx │ ├── ReportContainer.jsx │ └── SubheaderContainer.jsx ├── index.html ├── index.js ├── reducers │ ├── alertReducer.js │ ├── appReducer.js │ ├── diagramReducer.js │ ├── monitorReducer.js │ ├── rootReducer.js │ └── userReducer.js └── store.js ├── init.sql ├── package-lock.json ├── package.json ├── schema.sql ├── server ├── controllers │ ├── dbController.js │ ├── monitorController.js │ └── userController.js ├── models │ └── db.js ├── routers │ └── api.js ├── server.js └── socket.js ├── setup_db.sh ├── uri.txt └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/MoniQL/fb058c7965457a0ae8073b698cd5ac839e0de359/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es2021': true, 5 | 'node': true 6 | }, 7 | 'extends': [ 8 | 'eslint:recommended', 9 | 'plugin:react/recommended' 10 | ], 11 | 'overrides': [ 12 | { 13 | 'env': { 14 | 'node': true 15 | }, 16 | 'files': [ 17 | '.eslintrc.{js,cjs}' 18 | ], 19 | 'parserOptions': { 20 | 'sourceType': 'script' 21 | } 22 | } 23 | ], 24 | 'parserOptions': { 25 | 'ecmaVersion': 'latest', 26 | 'sourceType': 'module' 27 | }, 28 | 'plugins': [ 29 | 'react' 30 | ], 31 | 'rules': { 32 | 'indent': [ 33 | 'warn', 34 | 2 35 | ], 36 | 'linebreak-style': [ 37 | 'warn', 38 | 'unix' 39 | ], 40 | 'quotes': [ 41 | 'warn', 42 | 'single' 43 | ] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MoniQL: PostgreSQL Database Monitor and Visualiser 2 | 3 | ## Table of Contents 4 | 5 | - [Introduction](#introduction) 6 | - [Getting Started](#getting-started) 7 | - [Built With](#built-with) 8 | - [Interface & Features](#interface--features) 9 | - [Login/ Signup](#login--signup) 10 | - [Header and Sidebar](#header-and-sidebar) 11 | - [Dashboard](#dashboard) 12 | - [ERD](#erd) 13 | - [Monitors](#monitors) 14 | - [Alerts](#alerts) 15 | - [Stretch Features](#stretch-features) 16 | - [Contributing](#contributing) 17 | - [Contributors](#contributors) 18 | - [License](#license) 19 | 20 | ## Introduction 21 | 22 | MoniQL is a mighty PostgreSQL database monitor and visualiser. Designed to simplify and automate tasks involved in taking care of a relational database, MoniQL is here to keep your database safe and happy--MoniQL runs on queries only and does not set any constraints or triggers or anything else on your database! 23 | 24 | ## Getting Started 25 | 26 | To get started on monitoring your database with this Project: 27 | 28 | 1. Clone this repo to your local machine. 29 | 2. Run `npm install` for necessary dependencies. 30 | 3. Download and install PostgresQL to access MoniQL’s PostgresQL features. 31 | 4. Create a new remote (cloud-hosted) db instance to hold MoniQL's auth, monitors, and alerts data. [ElephantSQL](https://www.elephantsql.com/) offers free hosting. 32 | 5. Make sure the `setup_db.sh` script is executable by running `chmod +x setup_db.sh`. 33 | 6. On the command line run `./setup_db.sh`. 34 | 7. When prompted, provide the URI to your empty database where you want to store your MoniQL data. 35 | 8. Create a `.env` file in your root and add the following line: 36 | - `PG_URI=` 37 | 9. Create users with usernames, passwords, and URIs to the target cloud database to be monitored. 38 | 39 | ## Built With 40 | 41 | 42 | ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![Redux](https://img.shields.io/badge/redux-%23593d88.svg?style=for-the-badge&logo=redux&logoColor=white) ![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white) 43 | 44 | ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) ![Socket.io](https://img.shields.io/badge/Socket.io-black?style=for-the-badge&logo=socket.io&badgeColor=010101) ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) 45 | 46 | ![Webpack](https://img.shields.io/badge/webpack-%238DD6F9.svg?style=for-the-badge&logo=webpack&logoColor=black) ![Nodemon](https://img.shields.io/badge/NODEMON-%23323330.svg?style=for-the-badge&logo=nodemon&logoColor=%BBDEAD) ![Postman](https://img.shields.io/badge/Postman-FF6C37?style=for-the-badge&logo=postman&logoColor=white) 47 | 48 | - JavaScript 49 | - React 50 | - Redux 51 | - PostgreSQL 52 | - Material-UI 53 | - React-Flow 54 | - Node-Cron 55 | - Socket.io 56 | - Express.js 57 | - Webpack 58 | - Love <3 59 | 60 | ## Interface & Features 61 | 62 | ### Login/ Signup 63 | 64 | Upon application launch, you will be taken to the splash page with [currently dolphins]. Navigate to the login/sign up menu by clicking on the button on the top right. Login to your account associated with the database you want to monitor. Ensure that your account is registered correctly with a username, password, and URI. 65 | 66 | ### Header and Sidebar 67 | 68 | The bell icon in the header will display the number of unresolved alerts from the running monitors. Clicking on the bell icon will open a drawer on the right side where you have the option to click on an alert to see anomalous rows. You can also add a note, mark alerts as resolved, and clear resolved alerts by clicking on the x. 69 | 70 | The user silhouette icon will open up an option for Sign out so you can end your session. 71 | 72 | The sidebar has Dashboard, Focus Table ERD, Monitors, Alerts, and a settings icon for easy navigation. 73 | 74 | ### Dashboard 75 | 76 | Upon signing in to your MoniQL account, you will be presented with our Dashboard containing dynamic charts of your alerts by the specified date range or by status and a table with information about all your monitored tables. The Dashboard’s line chart and bar chart are responsively synced up to the time range selector above them, where users can choose a specific range of dates or jump to showing all of the Alerts of the past 7, 30, 60, or 90 days. The table of tables below the charts, listing each table currently under watch from one or more MoniQL Monitors, is initially organized by the number of downstream entities that a table has. But the table of tables is interactive, too, and can be reordered by clicking on any column of the table, letting users get a quick grasp of their data’s overall health. 77 | 78 | ![Dashboard Demo Animation](https://miro.medium.com/v2/resize:fit:640/format:webp/1*FA_r3s9H_7zQmx3TqCSJiA.gif) 79 | 80 | 81 | ### Entity Relationship Diagram 82 | 83 | The ERD icon will take you to MoniQL’s organized, interactive, and pleasingly styled Focus Table ERD where you can choose a table to focus on, and the depth of downstream tables that you will like to have displayed. This allows the user to choose any table from their database and immediately display that table’s downstream entity relationships, up to the chosen depth. 84 | 85 | ![ERD Demo Animation](https://miro.medium.com/v2/resize:fit:640/format:webp/1*g-RYLZZqvOGBaOQ2wLooqw.gif) 86 | 87 | ### Monitors 88 | 89 | The monitors icon will take you to the engine of MoniQL’s core monitoring functionality where you can view and edit a list of your active monitors. You can also filter monitors by table name, column name, and monitor type. On the right side of the screen, you can choose a type of Monitor to create—-so far we have full stack functionality for Range Monitors, Null Monitors, and Custom Monitors, and for each you can decide how often you’d like them to fire automatically, or you can fire at will with a button click. Range Monitors allow you to pick a minimum or a maximum value, or both, for any column that contains numbers, and set off an alert whenever a number outside of your chosen range is found. Our default Null Monitors look for null values in any column on a given table. And perhaps most useful to devs like yourself, our Custom Monitors allow you to enter whatever SQL query you’d like, set up to return anomalous values, and automatically check your database for such values at time intervals of your choosing. When one of your Monitors finds anomalous values on a table of yours, MoniQL will create an Alert, and update the bell icon in the upper right corner of your window. 90 | 91 | ![Monitors Demo Animation](https://miro.medium.com/v2/resize:fit:640/format:webp/1*ym_JZoLu-0zD4orPeuWgCQ.gif) 92 | 93 | ### Alerts 94 | 95 | The alerts icon will lead you to a closer view of your Alerts allowing you to filter by table name, column name, monitor type, and status. This will be where you have the option to click to see anomalous rows. You can also add notes, mark alerts as resolved, and clear resolved alerts by clicking on the x at the top right of the Alert component. Once Alerts have been dismissed, they will no longer display by default, but you can click the toggle on this page to see those you have dismissed as well as those that you haven’t gotten to yet. 96 | 97 | ![Alerts Demo Animation](https://miro.medium.com/v2/resize:fit:640/format:webp/1*SZi7OfIhOWRrzDeIk9hQqA.gif) 98 | 99 | ## Stretch Features 100 | 101 | - Support for other SQL databases: our current solution supports PostgreSQL databases with URIs, we would like to eventually support more database types. 102 | - More monitors! We have queries set up on the backend to return statistical data on any column holding numbers, but haven't yet implemented that on the frontend. 103 | - Query helper: we would like to incorporate a query helper that would assist in ensuring efficient and correct queries are being made. 104 | - Dark mode: styling to accommodate other color schemes. 105 | - Settings: additional settings for the app such as notification sounds. 106 | - External alerts: sending alerts as emails or connecting to other messaging systems such as Slack. 107 | 108 | ## Contributing 109 | 110 | We’ve released MoniQL as a valuable tool to help visualize and monitor PostgreSQL databases. Expect ongoing enhancements, extensions, and added features to be introduced to further elevate its utility. We deeply appreciate any community contributions and we invite you to explore MoniQL and offer suggestions for improvements as you see fit! If you encounter any issues, please report them in the issues tab or submit a PR. Your interest and support are greatly appreciated. 111 | 112 | ## Core Team 113 | 114 | - [Hay Nocik](https://github.com/haloxio) 115 | - [Aaron Brown](https://github.com/aarbrn) 116 | - [Chris Everett](https://github.com/Chris-Ever) 117 | - [King-Hur Wu](https://github.com/amrcnking) 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | moniQL 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | // External libraries 2 | import React, { useState, useEffect } from 'react'; 3 | import { useSelector } from 'react-redux'; 4 | import { CssBaseline, ThemeProvider } from '@mui/material'; 5 | 6 | // Components 7 | import LoginContainer from './components/Login'; 8 | import AppContainer from './containers/AppContainer'; 9 | import AlertBox from './components/AlertBox'; 10 | import Sidebar from './components/SideBar'; 11 | 12 | // Styles and themes 13 | import './components/stylesheets/App.css'; 14 | import { 15 | ColorModeContext, 16 | useMode, 17 | tokens, 18 | } from './components/stylesheets/Themes'; 19 | // import { CssBaseline, ThemeProvider } from "@mui/material"; 20 | // import Sidebar from './components/SideBar'; 21 | import LandingContainer from './containers/LandingContainer'; 22 | 23 | //for pull out drawer: 24 | // import Topbar from "./scenes/global/Topbar"; 25 | 26 | const App = () => { 27 | const isLoggedIn = useSelector((state) => state.user.isLoggedIn); 28 | 29 | //light/dark mode 30 | const [theme, colorMode] = useMode(); 31 | // const [isSideBar, setIsSideBar] = useState(true); 32 | 33 | return ( 34 | 35 | 36 | {/* */} 37 |
38 | {/* */} 39 | {/* */} 40 | {isLoggedIn ? : } 41 | {/* */} 42 |
43 |
44 |
45 | ); 46 | }; 47 | 48 | export default App; 49 | -------------------------------------------------------------------------------- /client/actions/actions.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | export const addAlertsActionCreator = (alertsArray) => ({ 4 | type: types.ADD_ALERTS, 5 | payload: alertsArray 6 | }) 7 | 8 | export const deleteAlertActionCreator = (alert_id) => ({ 9 | type: types.DELETE_ALERT, 10 | payload: alert_id 11 | }) 12 | 13 | export const updateAlertActionCreator = (alertObj) => ({ 14 | type: types.UPDATE_ALERT, 15 | payload: alertObj 16 | }) 17 | 18 | export const logInActionCreator = (user_id, username, uri) => ({ 19 | type: types.LOG_IN, 20 | payload: {user_id, username, uri} 21 | }) 22 | 23 | export const logOutActionCreator = () => ({ 24 | type: types.LOG_OUT 25 | }) 26 | 27 | export const saveDBActionCreator = (dbArray) => ({ 28 | type: types.SAVE_DB, 29 | payload: dbArray 30 | }) 31 | 32 | export const selectTableActionCreator = (tableName) => ({ 33 | type: types.SELECT_TABLE, 34 | payload: tableName 35 | }) 36 | 37 | export const selectDepthActionCreator = (depth) => ({ 38 | type: types.SELECT_DEPTH, 39 | payload: depth 40 | }) 41 | 42 | export const selectPageActionCreator = (page) => ({ 43 | type: types.SELECT_PAGE, 44 | payload: page 45 | }) 46 | 47 | export const addMonitorsActionCreator = (monitorsArray) => ({ 48 | type: types.ADD_MONITORS, 49 | payload: monitorsArray 50 | }) 51 | 52 | export const updateMonitorActionCreator = (monitorObj) => ({ 53 | type: types.UPDATE_MONITOR, 54 | payload: monitorObj 55 | }) 56 | 57 | export const addTablesWeightsActionCreator = (tablesWeightsObj) => ({ 58 | type: types.ADD_TABLES_WEIGHTS, 59 | payload: tablesWeightsObj 60 | }) 61 | 62 | export const displayAlertsActionCreator = (alertsArray) => ({ 63 | type: types.DISPLAY_ALERTS, 64 | payload: alertsArray 65 | }) 66 | 67 | export const displayMonitorsActionCreator = (monitorsArray) => ({ 68 | type: types.DISPLAY_MONITORS, 69 | payload: monitorsArray 70 | }) 71 | 72 | export const updateDashDisplayTimeRangeActionCreator = (timeRange) => ({ 73 | type: types.UPDATE_DASH_DISPLAY_TIME_RANGE, 74 | payload: timeRange 75 | }) -------------------------------------------------------------------------------- /client/components/AlertBar.jsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/MoniQL/fb058c7965457a0ae8073b698cd5ac839e0de359/client/components/AlertBar.jsx -------------------------------------------------------------------------------- /client/components/AlertBox.jsx: -------------------------------------------------------------------------------- 1 | // import react, redux, and react-redux, dispatch 2 | import React, { useState, useEffect } from 'react'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { Button, Alert, AlertTitle, TextField, Badge, Box, Stack, useTheme, Divider, Typography, Accordion, IconButton } from '@mui/material'; 5 | import { deleteAlertActionCreator, updateAlertActionCreator } from '../actions/actions.js'; 6 | import StorageIcon from '@mui/icons-material/Storage'; 7 | import dayjs from 'dayjs'; 8 | 9 | 10 | // regex check for timestamptz 11 | const timestamptzRegex = /(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)/; 12 | const checkTimestamptz = maybeTSTZ => { 13 | return timestamptzRegex.test(maybeTSTZ); 14 | } 15 | 16 | const AlertBox = (alertObj) => { 17 | 18 | const dispatch = useDispatch(); 19 | const username = useSelector((state) => state.user.username); 20 | const user_id = useSelector((state) => state.user.user_id); 21 | 22 | const alertObjCopy = {...alertObj}; 23 | const { alert_id, severity, table, column, rows, monitorType, anomalyType, anomalyValue, anomalyTime, detected_at, resolved_at, resolved, resolved_by, notes, display } = alertObjCopy; 24 | 25 | const queryAlerts = async (method, alertObj) => { 26 | try { 27 | const requestOptions = { 28 | method: method, 29 | headers: { 'Content-Type': 'application/json' }, 30 | body: JSON.stringify({ 31 | user_id: user_id, 32 | alertObj: alertObj 33 | }) 34 | }; 35 | const response = await fetch('/alerts', requestOptions); 36 | // const data = await response.json(); 37 | // console.log('data.alerts returned in queryAlerts func in alertBox', data); 38 | 39 | if (!response.ok) { 40 | throw new Error('Error from server in queryAlerts'); 41 | } else { 42 | method === 'PUT' ? dispatch(updateAlertActionCreator(alertObj)) : 43 | dispatch(deleteAlertActionCreator(alertObj.alert_id)); 44 | } 45 | 46 | } catch (error) { 47 | throw new Error(error); 48 | } 49 | }; 50 | 51 | const updateAlert = (alertObj) => { 52 | queryAlerts('PUT', alertObj); 53 | dispatch(updateAlertActionCreator(alertObj)); 54 | }; 55 | 56 | const addNotes = () => { 57 | 58 | if(newNote === '') return; 59 | 60 | const newNotesArr = notes.slice(); 61 | newNotesArr.push(newNote.concat(` -by ${username} at ` + dayjs().format('ddd YYYY-MM-DD hh:mm:ss a'))); 62 | 63 | 64 | const updatedAlertObj = { 65 | ...alertObj, 66 | notes: newNotesArr, 67 | } 68 | handleClickOpen(); 69 | return updateAlert(updatedAlertObj); 70 | } 71 | 72 | const markResolved = () => { 73 | if (resolved) { 74 | return; 75 | } else { 76 | const updatedAlertObj = { 77 | ...alertObj, 78 | resolved: true, 79 | resolved_at: dayjs().format('ddd YYYY-MM-DD hh:mm:ss a'), 80 | resolved_by: username 81 | } 82 | return updateAlert(updatedAlertObj); 83 | } 84 | } 85 | 86 | const [openNotes, setOpenNotes] = useState(false); 87 | const [newNote, setNewNote] = useState(''); 88 | 89 | const handleClickOpen = () => { 90 | setOpenNotes(!openNotes); 91 | } 92 | 93 | const handleNewNoteInput = (e) => { 94 | setNewNote(e.target.value); 95 | } 96 | 97 | // will conditionally render alerts by display value (in container component) 98 | // should include option to display resolved alerts, too 99 | const handleClose = () => { 100 | if(!resolved) return; 101 | const updatedAlertObj = { 102 | ...alertObj, 103 | display: false, 104 | } 105 | return updateAlert(updatedAlertObj); 106 | } 107 | 108 | const rowsCount = rows ? rows.length : null; 109 | 110 | // need to fix this! -A 111 | const unspool = rows => { 112 | const showRows = []; 113 | for(const row in rows){ 114 | let rowString = '• '; 115 | for(const key in rows[row]){ 116 | if(checkTimestamptz(rows[row][key])){ 117 | const timestampLegible = dayjs(rows[row][key]).format('ddd YYYY-MM-DD hh:mm:ss a'); 118 | rowString += `${key}: ${timestampLegible}, `; 119 | } else { 120 | rowString += `${key}: ${rows[row][key]}, `; 121 | } 122 | } 123 | rowString = rowString.trim().slice(0, -1); 124 | showRows.push(rowString); 125 | } 126 | return showRows; 127 | } 128 | 129 | const unspooledRows = rows ? unspool(rows) : null; 130 | 131 | const [rowsAccordionAnchorEl, setrowsAccordionAnchorEl] = useState(null); 132 | const handleRowsAccordionToggle = e => { 133 | if(rowsAccordionAnchorEl !== e.currentTarget){ 134 | setrowsAccordionAnchorEl(e.currentTarget); 135 | } else { 136 | setrowsAccordionAnchorEl(null); 137 | } 138 | } 139 | 140 | const renderRowsAccordion = ( 141 |
142 | 143 | 144 | 150 | 151 | 152 | 153 | click to see anomalous rows 154 | 155 |
156 | {rowsAccordionAnchorEl ? 157 | 158 | {rows ? unspooledRows.map((row, i) => {row}) : null} 159 | 160 | : null} 161 |
162 |
163 |
164 | ); 165 | 166 | // will deal with this later -- not MVP 167 | // const handleDeleteButton = () => { 168 | // return ( 169 | // Are you sure you want to delete this? Deletion is forever. 170 | // ) 171 | // } 172 | 173 | return display ? ( 174 | 175 | 190 | 191 | 192 | {monitorType} 193 | {" "} 194 | anomaly detected in{" "} 195 | 196 | {`${table}`}{" "} 197 | 198 | on{" "} 199 | 200 | {dayjs(detected_at).format("ddd MM-DD-YYYY")}{" "} 201 | 202 | at{" "} 203 | 204 | {dayjs(detected_at).format("hh:mm:ss a")}{" "} 205 | 206 | 207 | 208 | 209 | {column ? `Column: ${column}, ` : null} 210 | {anomalyType ? `Anomaly type: ${anomalyType}, ` : null} 211 | {anomalyValue ? `Anomaly value: ${anomalyValue}, ` : null} 212 | {anomalyTime 213 | ? `Anomaly time: ${dayjs(anomalyTime).format( 214 | "ddd MM-DD-YYYY hh:mm:ss a" 215 | )} ` 216 | : null} 217 | 218 | {renderRowsAccordion} 219 | 220 | {notes.length ? `Notes: ${notes.join(", ")}` : null} 221 | 222 | 223 | {openNotes ? ( 224 |
225 | 229 | 230 |
231 | ) : null} 232 |
233 | 234 | alert id: {alert_id} 235 | 236 | 237 |
238 | 239 | {/* {resolved ? `Resolved at ${resolved_at} by ${resolved_by}` : null} */} 240 | {resolved ? `Resolved at ${resolved_at} by ${username}` : null} 241 | {/* */} 242 | 243 |
244 | ) : null; 245 | } 246 | 247 | export default AlertBox; 248 | -------------------------------------------------------------------------------- /client/components/AlertsHeader.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Typography, 3 | Box, 4 | useTheme, 5 | Select, 6 | FormControl, 7 | FormControlLabel, 8 | InputLabel, 9 | MenuItem, 10 | Switch, 11 | Chip, 12 | } from '@mui/material'; 13 | import tokens from './stylesheets/Themes'; 14 | import React, { useEffect, useState } from 'react'; 15 | import { useSelector, useDispatch } from 'react-redux'; 16 | import { displayAlertsActionCreator } from '../actions/actions'; 17 | 18 | const SubHeader = ({ title, subtitle }) => { 19 | const theme = useTheme(); 20 | const colors = tokens(theme.palette.mode); 21 | return ( 22 | 23 | 29 | {title} 30 | 31 | 32 | {subtitle} 33 | 34 | 35 | ); 36 | }; 37 | 38 | const AlertsHeader = () => { 39 | const dispatch = useDispatch(); 40 | const alerts = useSelector((state) => state.alert.alerts); 41 | 42 | const [selectedTable, setSelectedTable] = useState(''); 43 | const [selectedColumn, setSelectedColumn] = useState(''); 44 | const [selectedMonitorType, setSelectedMonitorType] = useState(''); 45 | const [showResolved, setShowResolved] = useState(true); 46 | const [showDismissed, setShowDismissed] = useState(false); 47 | 48 | // look at alerts -- 49 | // pull out tables 50 | const tablesWithAlerts = []; 51 | alerts.forEach((alert) => { 52 | if (!tablesWithAlerts.includes(alert.table)) { 53 | tablesWithAlerts.push(alert.table); 54 | } 55 | }); 56 | 57 | // pull out columns 58 | const columnsOnTablesWithAlerts = {}; 59 | alerts.forEach((alert) => { 60 | if (!columnsOnTablesWithAlerts[alert.table]) { 61 | columnsOnTablesWithAlerts[alert.table] = []; 62 | } 63 | if (!columnsOnTablesWithAlerts[alert.table].includes(alert.column)) { 64 | columnsOnTablesWithAlerts[alert.table].push(alert.column); 65 | } 66 | }); 67 | 68 | // pull out monitor types 69 | const monitorTypes = []; 70 | alerts.forEach((alert) => { 71 | if (!monitorTypes.includes(alert.monitorType)) { 72 | monitorTypes.push(alert.monitorType); 73 | } 74 | }); 75 | 76 | // dropdown for table 77 | // dropdown for column 78 | // dropdown for monitor type 79 | // toggle for resolved/unresolved 80 | // toggle for dismissed/not dismissed (display property of alertObj) 81 | 82 | useEffect(() => { 83 | const filteredAlerts = alerts.filter((alert) => { 84 | if (selectedTable && alert.table !== selectedTable) { 85 | return false; 86 | } 87 | if (selectedColumn && alert.column !== selectedColumn) { 88 | return false; 89 | } 90 | if (selectedMonitorType && alert.monitorType !== selectedMonitorType) { 91 | return false; 92 | } 93 | if (!showResolved && alert.resolved) { 94 | return false; 95 | } 96 | if (!showDismissed && !alert.display) { 97 | return false; 98 | } 99 | 100 | if (showDismissed && alert.display === false) { 101 | return true; 102 | } 103 | return true; 104 | }); 105 | dispatch(displayAlertsActionCreator(filteredAlerts)); 106 | }, [ 107 | selectedTable, 108 | selectedColumn, 109 | selectedMonitorType, 110 | showResolved, 111 | showDismissed, 112 | alerts, 113 | ]); 114 | 115 | return ( 116 |
117 | 118 | 122 | 123 | 124 | 125 | 126 | Table Name 127 | 143 | 144 | 145 | Column Name 146 | 165 | 166 | 167 | 168 | Monitor Type 169 | 170 | 186 | 187 | 188 | 194 | 199 | {/* Display Resolved Alerts */} 200 | setShowResolved(!showResolved)} 205 | /> 206 | } 207 | label='Display Resolved Alerts' 208 | labelPlacement='start' 209 | // 210 | /> 211 | 212 | 217 | {/* Display Dismissed Alerts */} 218 | setShowDismissed(!showDismissed)} 223 | /> 224 | } 225 | label='Display Dismissed Alerts' 226 | labelPlacement='start' 227 | // 228 | /> 229 | 230 | { 233 | setSelectedTable(''); 234 | setSelectedColumn(''); 235 | setSelectedMonitorType(''); 236 | setShowResolved(true); 237 | setShowDismissed(false); 238 | }} 239 | size='large' 240 | sx={{ 241 | fontSize: '1.2em', 242 | mt: 2, 243 | ml: 3, 244 | }} 245 | /> 246 | 247 | 248 | 249 |
250 | ); 251 | }; 252 | 253 | export default AlertsHeader; 254 | -------------------------------------------------------------------------------- /client/components/DashAlertBarChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BarChart } from '@mui/x-charts'; 3 | import { Box, Divider } from '@mui/material'; 4 | import { useSelector } from 'react-redux'; 5 | import dayjs from "dayjs"; 6 | 7 | const DashAlertBarChart = () => { 8 | 9 | // look at alerts in state 10 | // pull out data -- how many alerts of each status: unresolved, resolved, not dismissed, dismissed 11 | const alerts = useSelector((state) => state.alert.alerts); 12 | const dashDisplayAlertsTimeRange = useSelector((state) => state.diagram.dashDisplayAlertsTimeRange); 13 | 14 | const alertsInTimeRange = alerts.filter((alertObj) => { 15 | return dayjs(alertObj.detected_at).isAfter(dayjs(dashDisplayAlertsTimeRange[0])) && dayjs(alertObj.detected_at).isBefore((dashDisplayAlertsTimeRange[1])) 16 | }); 17 | 18 | const alertsByStatus = { 19 | unresolved: 0, 20 | resolved: 0, 21 | notDismissed: 0, 22 | dismissed: 0 23 | }; 24 | 25 | alertsInTimeRange.forEach(alertObj => { 26 | alertObj.resolved ? alertsByStatus.resolved++ : alertsByStatus.unresolved++; 27 | alertObj.display ? alertsByStatus.notDismissed++ : alertsByStatus.dismissed++; 28 | }); 29 | 30 | return ( 31 | 39 |

Alerts by status

40 | 41 | 42 | 76 |
77 | ); 78 | 79 | }; 80 | 81 | export default DashAlertBarChart; -------------------------------------------------------------------------------- /client/components/DashAlertLineChart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LineChart } from '@mui/x-charts/LineChart'; 3 | import { useSelector } from 'react-redux'; 4 | import dayjs from 'dayjs'; 5 | import { Box, Divider } from '@mui/material'; 6 | 7 | const DashAlertLineChart = () => { 8 | 9 | const alerts = useSelector((state) => state.alert.alerts); 10 | const dashDisplayAlertsTimeRangeFromState = useSelector((state) => state.diagram.dashDisplayAlertsTimeRange); 11 | 12 | const dashDisplayAlertsTimeRangeSize = dashDisplayAlertsTimeRangeFromState[1] - dashDisplayAlertsTimeRangeFromState[0]; 13 | const dashDisplayAlertsTimeRangeInDays = Math.floor(dashDisplayAlertsTimeRangeSize / 86400000); 14 | const dashDisplayAlertsTimeRangeInterval = Math.ceil(dashDisplayAlertsTimeRangeInDays / 7); 15 | 16 | let numIntervals = 7; 17 | if (dashDisplayAlertsTimeRangeInDays < 7) { 18 | numIntervals = dashDisplayAlertsTimeRangeInDays; 19 | } 20 | 21 | // look at alerts in state 22 | // pull out data -- how many alerts over each of the last 7 days 23 | // date on alert object: alertObj.detected_at 24 | const days = []; 25 | // populate days array with dates from beginning of time range to end of time range (7 steps) 26 | for (let i = numIntervals; i > 0; i--) { 27 | days.push(dayjs(dashDisplayAlertsTimeRangeFromState[1]).subtract((i - 1) * dashDisplayAlertsTimeRangeInterval, 'day').format('YYYY-MM-DD')); 28 | } 29 | // console.log('days in dashalertlinechart', days) 30 | 31 | // create an array to hold the number of alerts for each day 32 | const alertsByInterval = new Array(numIntervals).fill(0); 33 | // populate alertsByDay array with the number of alerts for each day 34 | for(let i = 0; i < alerts.length; i++){ 35 | let j = 0; 36 | while(j < numIntervals) { 37 | const alertDate = dayjs(alerts[i].detected_at).format('YYYY-MM-DD'); 38 | if(alertDate > days[j] && alertDate <= days[j + 1]) { 39 | alertsByInterval[j] = alertsByInterval[j] + 1; 40 | } 41 | j++; 42 | } 43 | } 44 | 45 | // console.log('alertsByInterval in dashalertlinechart', alertsByInterval) 46 | 47 | return ( 48 | 62 |

Alerts by date

63 | 64 | 65 | 93 |
94 | ); 95 | }; 96 | 97 | export default DashAlertLineChart; -------------------------------------------------------------------------------- /client/components/DashTableOfTables.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector, useStore, useDispatch } from 'react-redux'; 3 | import {Box, Typography} from '@mui/material'; 4 | import { DataGrid } from '@mui/x-data-grid'; 5 | import { addTablesWeightsActionCreator } from '../actions/actions'; 6 | 7 | const DashTableOfTables = () => { 8 | const store = useStore(); 9 | const dispatch = useDispatch(); 10 | 11 | const monitors = useSelector((state) => state.monitor.activeMonitors); 12 | const alerts = useSelector((state) => state.alert.alerts); 13 | const tableMetadata = useSelector((state) => state.diagram.data); 14 | 15 | const [dashMonitorData, setDashMonitorData] = useState({}); 16 | const [dashToTRows, setdashToTRows] = useState([]); 17 | const [tablesWeightsObj, setTablesWeightsObj] = useState(null); 18 | 19 | const [didPopulateDashMonitorObj, setDidPopulateDashMonitorObj] = useState(false); 20 | const [didGetDashMonitorData, setDidGetDashMonitorData] = useState(false); 21 | 22 | // look at alerts in state 23 | // pull out data -- how many monitors are looking at each table 24 | // table on monitor object: monitorObj.params.table 25 | 26 | const populateDashMonitorObj = () => { 27 | const newDashMonitorData = { ...dashMonitorData }; 28 | let hasChanged = false; 29 | 30 | monitors.forEach((monitorObj) => { 31 | // console.log('monitorObj in popDMDO in dashToT', monitorObj) 32 | const table = monitorObj.parameters.table; 33 | if (newDashMonitorData[table]) { 34 | newDashMonitorData[table].numMonitors++; 35 | hasChanged = true; 36 | } else { 37 | newDashMonitorData[table] = { 38 | numDownstream: 0, 39 | numMonitors: 1, 40 | numAlerts: 0, 41 | numUnresolved: 0, 42 | numResolved: 0, 43 | numNotDismissed: 0, 44 | numDismissed: 0, 45 | numRange: 0, 46 | numNull: 0, 47 | numCustom: 0 48 | }; 49 | hasChanged = true; 50 | } 51 | }); 52 | 53 | if (hasChanged){ 54 | setDashMonitorData(newDashMonitorData); 55 | // console.log('dashMonitorData just populated', dashMonitorData) 56 | setDidPopulateDashMonitorObj(true); 57 | } 58 | }; 59 | 60 | useEffect(() => { 61 | populateDashMonitorObj(); 62 | }, [monitors]); 63 | 64 | useEffect(() => { 65 | console.log('dashMonitorData updated', dashMonitorData); 66 | }, [dashMonitorData]); 67 | 68 | const getDashMonitorData = () => { 69 | 70 | const newDashMonitorData = { ...dashMonitorData }; 71 | 72 | // console.log('alerts in getDashMonitorData in dashToT', alerts) 73 | 74 | alerts.forEach((alertObj) => { 75 | const { table, resolved, display, monitorType } = alertObj; 76 | // console.log('table in getDashMonitorData in dashToT', table, 'resolved', resolved, 'display', display, 'monitorType', monitorType) 77 | if(newDashMonitorData[table]) { 78 | newDashMonitorData[table].numAlerts++; 79 | resolved ? newDashMonitorData[table].numResolved++ : newDashMonitorData[table].numUnresolved++; 80 | display ? newDashMonitorData[table].numNotDismissed++ : newDashMonitorData[table].numDismissed++; 81 | newDashMonitorData[table][`num${monitorType}`]++; 82 | } 83 | }); 84 | 85 | setDashMonitorData(newDashMonitorData); 86 | setDidGetDashMonitorData(true); 87 | }; 88 | 89 | // columns: 90 | // id 91 | // table name 92 | // number of monitors 93 | // number of alerts 94 | // number of unresolved alerts 95 | // number of resolved alerts 96 | // number of displayed alerts 97 | // number of hidden alerts 98 | // number of alerts of type range 99 | // number of alerts of type null 100 | // number of alerts of type custom 101 | 102 | const dashToTColumns = [ 103 | { field: 'id', headerName: 'ID', width: 30}, 104 | { field: 'table', headerName: 'Table', width: 180}, 105 | { field: 'downstream', headerName: 'Downstream Entities', width: 130}, 106 | { field: 'monitors', headerName: 'Monitors', width: 85}, 107 | { field: 'alerts', headerName: 'Alerts', width: 85}, 108 | { field: 'unresolved', headerName: 'Unresolved', width: 85}, 109 | { field: 'resolved', headerName: 'Resolved', width: 85}, 110 | { field: 'displayed', headerName: 'Displayed', width: 85}, 111 | { field: 'dismissed', headerName: 'Dismissed', width: 85}, 112 | { field: 'range', headerName: 'Range', width: 85}, 113 | { field: 'null', headerName: 'Null', width: 85}, 114 | { field: 'custom', headerName: 'Custom', width: 85} 115 | ]; 116 | 117 | // turn out: array of rows 118 | 119 | // each row: 120 | // table name 121 | // number of monitors 122 | // number of alerts 123 | // number of unresolved alerts 124 | // number of resolved alerts 125 | // number of displayed alerts 126 | // number of hidden alerts 127 | // number of alerts of type range 128 | // number of alerts of type null 129 | 130 | const populateDashToTRows = () => { 131 | // if(!tablesWeightsObj) return; 132 | // console.log('popDToTR called') 133 | // console.log('tablesWeightsObj in DToT: ', tablesWeightsObj); 134 | // console.log('dashMonitorData in DToT: ', dashMonitorData) 135 | 136 | const newDashToTRows = []; 137 | for(const tableInDMD in dashMonitorData){ 138 | const { numMonitors, numAlerts, numUnresolved, numResolved, 139 | numNotDismissed, numDismissed, numRange, numNull, numCustom, numDownstream } = dashMonitorData[tableInDMD]; 140 | // console.log('tableInDMD', tableInDMD) 141 | // console.log('dashMonitorData', dashMonitorData) 142 | 143 | newDashToTRows.push({ 144 | table: tableInDMD, 145 | downstream: numDownstream, 146 | monitors: numMonitors, 147 | alerts: numAlerts, 148 | unresolved: numUnresolved, 149 | resolved: numResolved, 150 | displayed: numNotDismissed, 151 | dismissed: numDismissed, 152 | range: numRange, 153 | null: numNull, 154 | custom: numCustom 155 | }); 156 | } 157 | // sort in descending order of numDownstream 158 | newDashToTRows.sort((a, b) => { 159 | return b.downstream - a.downstream; 160 | }); 161 | 162 | newDashToTRows.forEach((row, i) => { 163 | row.id = i + 1; 164 | }); 165 | 166 | setdashToTRows(newDashToTRows); 167 | }; 168 | 169 | useEffect(() => { 170 | if(Object.keys(dashMonitorData).length) getDashMonitorData(); 171 | }, [didPopulateDashMonitorObj, alerts]); 172 | 173 | // do not delete -- only commenting this out until we move the tableWeight function to a helper file 174 | // useEffect(() => { 175 | // if (didGetDashMonitorData){ 176 | // const tablesWeightsObj = store.getState().diagram.tablesWeightsObj; 177 | // console.log('tablesWeightsObj in getDashMonitorData in dashToT', tablesWeightsObj) 178 | // setTablesWeightsObj(tablesWeightsObj); 179 | // } 180 | // }, [didGetDashMonitorData]); 181 | 182 | // useEffect(() => { 183 | // console.log('tablesWeightsObj updated: ', tablesWeightsObj) 184 | // }, [tablesWeightsObj]); 185 | 186 | useEffect(() => { 187 | if(didGetDashMonitorData && tablesWeightsObj){ 188 | // console.log('tablesWeightsObj in getDashMonitorData in dashToT', tablesWeightsObj) 189 | for(const tableInTablesWeightsObj in tablesWeightsObj){ 190 | if(dashMonitorData[tableInTablesWeightsObj]){ 191 | dashMonitorData[tableInTablesWeightsObj].numDownstream = tablesWeightsObj[tableInTablesWeightsObj]; 192 | } 193 | } 194 | populateDashToTRows(); 195 | } 196 | }, [didGetDashMonitorData, tablesWeightsObj, alerts]); 197 | 198 | // pasting this function from Focus.jsx -- should be in a helper file -- then uncomment useEffect above 199 | const tableWeight = () => { 200 | const importance = new Map(); 201 | tableMetadata.forEach((table) => { 202 | //table name = key / num of FKs (<< hehe) = value 203 | importance.set(table.table_name, (table.foreign_keys || []).length); 204 | }); 205 | 206 | // if(tableWeightNotCalledYet) { 207 | const importanceObj = Object.fromEntries(importance); 208 | dispatch(addTablesWeightsActionCreator(importanceObj)); 209 | setTablesWeightsObj(importanceObj); 210 | // tableWeightNotCalledYet.current = false; 211 | // } 212 | 213 | return importance; 214 | }; 215 | 216 | useEffect(() => { 217 | // console.log('calling tableWeight in dashToT') 218 | tableWeight(); 219 | }, [didGetDashMonitorData]); 220 | 221 | return ( 222 | 223 | 231 | {/* Tables */} 232 | 240 | {/* monitored tables */} 241 |

Monitored tables

242 | 258 |
259 |
260 |
261 | ); 262 | }; 263 | 264 | export default DashTableOfTables; -------------------------------------------------------------------------------- /client/components/DashboardHeader.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Typography, 3 | Box, 4 | useTheme, 5 | Divider, 6 | Chip, 7 | FormControl, 8 | FormHelperText 9 | } from "@mui/material"; 10 | import tokens from "./stylesheets/Themes"; 11 | import React, { useState } from "react"; 12 | import { DatePicker } from "@mui/x-date-pickers"; 13 | import dayjs from "dayjs"; 14 | import { useSelector, useDispatch } from "react-redux"; 15 | import { updateDashDisplayTimeRangeActionCreator } from "../actions/actions"; 16 | 17 | const SubHeader = ({ title, subtitle }) => { 18 | const theme = useTheme(); 19 | const colors = tokens(theme.palette.mode); 20 | return ( 21 | 22 | 28 | {title} 29 | 30 | 31 | {subtitle} 32 | 33 | 34 | ); 35 | }; 36 | 37 | const DashboardHeader = () => { 38 | const dispatch = useDispatch(); 39 | const dashDisplayAlertsTimeRangeFromState = useSelector( 40 | (state) => state.diagram.dashDisplayAlertsTimeRange 41 | ); 42 | const dashDisplayAlertsTimeRangeAsDayjs = 43 | dashDisplayAlertsTimeRangeFromState.map((date) => dayjs(date)); 44 | const [dashDisplayAlertsTimeRange, setDashDisplayAlertsTimeRange] = useState( 45 | dashDisplayAlertsTimeRangeAsDayjs 46 | ); 47 | 48 | const handleDashDisplayAlertsTimeRange = (timeRangeArr) => { 49 | const timeRangeArrAsTimestamps = timeRangeArr.map((date) => date.valueOf()); 50 | dispatch(updateDashDisplayTimeRangeActionCreator(timeRangeArrAsTimestamps)); 51 | setDashDisplayAlertsTimeRange(timeRangeArr.map(dayjs)); 52 | console.log("timeRangeArr", timeRangeArr); 53 | }; 54 | 55 | return ( 56 |
57 | 58 | 62 | 63 | 71 | 72 | 77 | handleDashDisplayAlertsTimeRange([ 78 | newValue, 79 | dashDisplayAlertsTimeRange[1], 80 | ]) 81 | } 82 | sx={{ 83 | backgroundColor: "#2E2D3D", 84 | borderRadius: "5px", 85 | }} 86 | /> 87 | 88 | Pick start date for lookback range 89 | 90 | 91 | 94 | 100 | 101 | 102 | 106 | handleDashDisplayAlertsTimeRange([ 107 | dashDisplayAlertsTimeRange[0], 108 | newValue, 109 | ]) 110 | } 111 | sx={{ 112 | backgroundColor: "#2E2D3D", 113 | borderRadius: "5px", 114 | }} 115 | /> 116 | Pick end date for lookback range 117 | 118 | 130 | 133 | handleDashDisplayAlertsTimeRange([ 134 | dayjs().subtract(7, "day"), 135 | dayjs(), 136 | ]) 137 | } 138 | sx={{ mr: 1, p: 0.5 }} 139 | /> 140 | 143 | handleDashDisplayAlertsTimeRange([ 144 | dayjs().subtract(30, "day"), 145 | dayjs(), 146 | ]) 147 | } 148 | sx={{ mr: 1, p: 0.5 }} 149 | /> 150 | 153 | handleDashDisplayAlertsTimeRange([ 154 | dayjs().subtract(60, "day"), 155 | dayjs(), 156 | ]) 157 | } 158 | sx={{ mr: 1, p: 0.5 }} 159 | /> 160 | 163 | handleDashDisplayAlertsTimeRange([ 164 | dayjs().subtract(90, "day"), 165 | dayjs(), 166 | ]) 167 | } 168 | sx={{ mr: 1, p: 0.5 }} 169 | /> 170 | 171 | 172 | 173 | 174 |
175 | ); 176 | }; 177 | 178 | export default DashboardHeader; 179 | -------------------------------------------------------------------------------- /client/components/ErdVisualizer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import ReactFlow, { 4 | MiniMap, 5 | Controls, 6 | Background, 7 | useNodesState, 8 | useEdgesState, 9 | addEdge, 10 | } from 'reactflow'; 11 | 12 | //must import reactflow css for visualizer to work 13 | import 'reactflow/dist/style.css'; 14 | 15 | const ErdVisualizer = () => { 16 | const initialNodes = []; 17 | const initialEdges = []; 18 | const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); 19 | const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); 20 | const data = useSelector((state) => state.diagram.data); 21 | 22 | //this is where we will map over the data and create nodes 23 | //edges will ALSObe created here 24 | useEffect(() => { 25 | const newNodes = []; 26 | const newEdges = []; 27 | 28 | data.forEach((table, i) => { 29 | const label = ( 30 |
31 |

{table.table_name}

32 | 37 |
38 | ); 39 | newNodes.push({ 40 | id: `${table.table_name}`, 41 | position: { x: 150 * i, y: 0 }, 42 | data: { label }, 43 | }); 44 | 45 | if (table.foreign_keys) { 46 | table.foreign_keys.forEach((fk) => 47 | newEdges.push({ 48 | id: `e${table.table_name}-${fk.foreign_table}`, 49 | source: `${table.table_name}`, 50 | target: `${fk.foreign_table}`, 51 | }) 52 | ); 53 | } 54 | }); 55 | 56 | setNodes(newNodes); 57 | setEdges(newEdges); 58 | }, [data, setNodes, setEdges]); 59 | // const onConnect = useCallback( 60 | // (params) => setEdges((eds) => addEdge(params, eds)), 61 | // [setEdges] 62 | // ); 63 | return ( 64 |
65 |

BERD VIZISUALMALIZERATOR 3000

66 | 73 | 74 | 75 | 76 | 77 |
78 | ); 79 | }; 80 | 81 | export default ErdVisualizer; 82 | -------------------------------------------------------------------------------- /client/components/Focus.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useMemo, useEffect, useState, memo } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { addTablesWeightsActionCreator } from '../actions/actions'; 4 | import ReactFlow, { 5 | MiniMap, 6 | Controls, 7 | Background, 8 | Handle, 9 | Position, 10 | useNodesState, 11 | useEdgesState, 12 | addEdge, 13 | isEdge, 14 | Panel, 15 | } from 'reactflow'; 16 | //must import reactflow css for visualizer to work 17 | import 'reactflow/dist/style.css'; 18 | import { Card, CardContent, Typography, List } from '@mui/material'; 19 | // import style from "./stylesheets/visualizer"; 20 | import styled, { ThemeProvider } from 'styled-components'; 21 | import { ReactFlowProvider } from 'react-flow-renderer'; 22 | //This 23 | ////////////////////////////**********HAY STACK**********////////////////////////////// 24 | ////////////////////////////**********HAY STACK**********////////////////////////////// 25 | 26 | const lightTheme = { 27 | bg: '#fff', 28 | primary: '#ff0072', 29 | 30 | nodeBg: '#f2f2f5', 31 | nodeColor: '#222', 32 | nodeBorder: '#222', 33 | 34 | minimapMaskBg: '#f2f2f5', 35 | 36 | controlsBg: '#fefefe', 37 | controlsBgHover: '#eee', 38 | controlsColor: '#222', 39 | controlsBorder: '#ddd', 40 | }; 41 | const darkTheme = { 42 | bg: '#000', 43 | primary: '#ff0072', 44 | 45 | nodeBg: '#343435', 46 | nodeColor: '#f9f9f9', 47 | nodeBorder: '#888', 48 | 49 | minimapMaskBg: '#343435', 50 | 51 | controlsBg: '#555', 52 | controlsBgHover: '#676768', 53 | controlsColor: '#dddddd', 54 | controlsBorder: '#676768', 55 | }; 56 | // const CustomNode = styled.div` 57 | // padding: 10px 20px; 58 | // border-radius: 5px; 59 | // width: 8px; 60 | // height: 10px; 61 | // background: ${(props) => props.theme.nodeBg}; 62 | // color: ${(props) => props.theme.nodeColor}; 63 | // border: 1px solid 64 | // ${(props) => 65 | // props.selected ? props.theme.primary : props.theme.nodeBorder}; 66 | 67 | // .react-flow__handle { 68 | // background: ${(props) => props.theme.primary}; 69 | // width: 8px; 70 | // height: 10px; 71 | // border-radius: 3px; 72 | // } 73 | // `; 74 | 75 | ////////////////////////////**********HAY STACK**********////////////////////////////// 76 | 77 | ////////////////////////////**********HAY STACK**********////////////////////////////// 78 | 79 | const Focus = ({ children, elements }) => { 80 | const initialNodes = []; 81 | const initialEdges = []; 82 | const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); 83 | const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); 84 | const data = useSelector((state) => state.diagram.data); 85 | const focusTable = useSelector((state) => state.diagram.focusTable); 86 | const focusDepth = useSelector((state) => state.diagram.depth); 87 | const dispatch = useDispatch(); 88 | 89 | ///////////////////////NODE STYLE///////////////////////// 90 | 91 | const nodeStyle = { 92 | width: '150px', // Fixed width 93 | height: '200px', // Fixed height 94 | // border: "1px solid #777", 95 | 96 | // padding: 20, 97 | }; 98 | // const listStyle = { 99 | // textAlign: 'left', 100 | // } 101 | 102 | const CustomNode = ({ data }) => { 103 | return ( 104 |
105 | 106 | 111 | 112 |

{data.label}

113 |
    114 | {data.columns.map((column, index) => ( 115 |
  • {column}
  • 116 | ))} 117 |
118 | { data.fks.map((fk, i) => ( 119 | 126 | )) 127 | } 128 |
129 | ); 130 | }; 131 | 132 | ///////////////////////NODE STYLE///////////////////////// 133 | 134 | const nodeTypes = useMemo(() => ({ custom: CustomNode }), []); 135 | 136 | const tableWeightNotCalledYet = useRef(true); 137 | 138 | useEffect(() => { 139 | if(!data.length) return; 140 | console.log('THIS IS FOCUS DATA', data); 141 | console.log(`THIS IS DEPTH ${focusDepth}`); 142 | const newNodes = []; 143 | const newEdges = []; 144 | 145 | ////////////////////////////**********HAY WEIGHT/IMPORTANCE ALGO**********////////////////////////////// 146 | /////*** this should go somewhere else so that we can also use in our dash WE B DRY ;) ***///// 147 | //calc weight/importance of each table (by # of FKs (<< hehe)) 148 | const tableWeight = () => { 149 | const importance = new Map(); 150 | data.forEach((table) => { 151 | //table name = key / num of FKs (<< hehe) = value 152 | importance.set(table.table_name, (table.foreign_keys || []).length); 153 | }); 154 | console.log('IMPORTANCEeeee!!!!!!!!!!', importance); 155 | 156 | if(tableWeightNotCalledYet) { 157 | const importanceObj = Object.fromEntries(importance); 158 | dispatch(addTablesWeightsActionCreator(importanceObj)); 159 | tableWeightNotCalledYet.current = false; 160 | console.log('dispatched table weights: ', importanceObj); 161 | } 162 | 163 | return importance; 164 | }; 165 | 166 | //sort tables by weight/importance 167 | const sortTables = (tableWeight) => { 168 | //sorts arr by num of FKs (<< hehe) (descending) 169 | return [...data].sort( 170 | (a, b) => tableWeight.get(b.table_name) - tableWeight.get(a.table_name) 171 | ); 172 | }; 173 | 174 | 175 | ////////////////////////////**********HAY WEIGHT/IMPORTANCE ALGO**********////////////////////////////// 176 | 177 | //our variables for rendering below (add on between add table and foreach replace data with sortedData) 178 | const weightMap = tableWeight(); 179 | const sortedTables = sortTables(weightMap); 180 | console.log('SORTED!!!!!!!!!!', sortedTables); 181 | 182 | const addTable = (currTable, pizza = 0, xVal = 0, yVal = 0) => { 183 | console.log( 184 | `NEW CALL OF addTable. table now = '${currTable}' xVal now = ${xVal}` 185 | ); 186 | if (xVal > pizza) { 187 | return; 188 | } 189 | 190 | const table = sortedTables.find( 191 | (table) => table.table_name === currTable 192 | ); 193 | if (table) { 194 | const columnArray = table.columns.map((column) => column.name); 195 | 196 | // if (table.table_name === currTable) { 197 | // const columnArray = table.columns.map((column) => column.name); //grab the column names from each table's column array 198 | 199 | newNodes.push({ 200 | id: `${table.table_name}`, 201 | type: 'custom', 202 | position: { x: 100 + 300 * xVal, y: 100 + 250 * yVal }, 203 | data: { label: `${table.table_name}`, columns: columnArray, fks: table.foreign_keys ? table.foreign_keys : [] }, 204 | }); 205 | 206 | console.log('table.foreign keys in Focus', table.foreign_keys) 207 | 208 | //iterate thru foreign keys property of table 209 | if (table.foreign_keys) { 210 | // xVal += 1; 211 | const sortedForeignKeys = table.foreign_keys.sort((a, b) => { 212 | return weightMap.get(b.foreign_table) - weightMap.get(a.foreign_table); 213 | }); 214 | sortedForeignKeys.forEach((key, j) => { 215 | newEdges.push({ 216 | id: `e${currTable}-${key.foreign_table}-${j}`, 217 | source: `${currTable}`, 218 | target: `${key.foreign_table}`, 219 | sourceHandle: `source-${key.foreign_table}`, 220 | }); 221 | addTable(key.foreign_table, pizza, xVal + 1, j); 222 | }); 223 | } 224 | } 225 | }; 226 | addTable(focusTable, focusDepth); 227 | // console.log(newNodes) 228 | console.log('Nodes:', newNodes); 229 | console.log('Edges:', newEdges); 230 | setNodes(newNodes); 231 | setEdges(newEdges); 232 | }, [data, focusTable, focusDepth]); // runs whenever `data` or `currNode` changes 233 | 234 | return ( 235 |
236 | 244 | {/* */} 245 | 246 | 247 | 248 | 249 |
250 | ); 251 | }; 252 | 253 | export default Focus; 254 | -------------------------------------------------------------------------------- /client/components/FocusBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { 4 | Box, 5 | useTheme, 6 | Select, 7 | MenuItem, 8 | Typography, 9 | createTheme, 10 | ThemeProvider, 11 | InputLabel, 12 | FormHelperText, 13 | FormControl, 14 | FormControlLabel, 15 | FormGroup, 16 | Switch, 17 | NativeSelect, 18 | } from '@mui/material'; 19 | import { 20 | selectTableActionCreator, 21 | selectDepthActionCreator, 22 | } from '../actions/actions'; 23 | import tokens from './stylesheets/Themes'; 24 | 25 | //////////////////////import subheader///////////////////// 26 | // import SubHeader from "./SubHeader"; 27 | const SubHeader = ({ title, subtitle }) => { 28 | const theme = useTheme(); 29 | const colors = tokens(theme.palette.mode); 30 | return ( 31 | 32 | 38 | {title} 39 | 40 | 41 | {/* {subtitle} */} 42 | 43 | 44 | ); 45 | }; 46 | // export default SubHeader; 47 | //////////////////////import subheader///////////////////// 48 | 49 | const FocusBar = () => { 50 | ////////////DLT AFTER IMPORTING SUBHEADER//////////////// 51 | const theme = useTheme(); 52 | const colors = tokens(theme.palette.mode); 53 | ////////////DLT AFTER IMPORTING SUBHEADER//////////////// 54 | 55 | const [focus, setFocus] = useState(''); 56 | const [depth, setDepth] = useState(''); 57 | const [depthOptions, setDepthOptions] = useState([]); 58 | // const [direction, setDirection] = useState(""); 59 | const tablesWeightsObj = useSelector((state) => state.diagram.tablesWeightsObj); 60 | const dispatch = useDispatch(); 61 | 62 | const handleFocus = (tableName) => { 63 | console.log('new focus in handleFocus in FocusBar: ', tableName); 64 | setFocus(tableName); 65 | dispatch(selectTableActionCreator(tableName)); 66 | }; 67 | 68 | const focusItems = useSelector((state) => state.diagram.data || []); 69 | 70 | const handleDepth = (num) => { 71 | console.log(num); 72 | setDepth(num); 73 | dispatch(selectDepthActionCreator(num)); 74 | }; 75 | 76 | //fix this please 77 | // const depthOptions = Array.from({ length: 7 }, (_, i) => i); 78 | // const directionSet = useSelector((state) => state.diagram.direction || "horizontal"); 79 | 80 | useEffect(() => { 81 | if(!focus) return; 82 | setDepthOptions(Array.from({ length: tablesWeightsObj[focus] + 1}, (_, i) => i)); 83 | }, [focus]); 84 | 85 | return ( 86 |
87 | {/* */} 88 | {/* //PAGE HEADER */} 89 | {/* SQL Visualizer */} 90 | 91 | 95 | 96 | 97 | Table Name 98 | 113 | Select table name to focus on 114 | 115 | 116 | Depth 117 | 132 | Select Depth 133 | 134 | 135 | } 137 | label="Portrait View" 138 | /> 139 | 140 | 141 | 142 | {/* */} 143 |
144 | ); 145 | }; 146 | 147 | export default FocusBar; 148 | -------------------------------------------------------------------------------- /client/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { logOutActionCreator } from '../actions/actions'; 4 | import { styled, alpha } from '@mui/material/styles'; 5 | import { 6 | Drawer, 7 | AppBar, 8 | Box, 9 | Toolbar, 10 | IconButton, 11 | InputBase, 12 | Badge, 13 | MenuItem, 14 | Menu, 15 | createTheme, 16 | ThemeProvider, 17 | Divider, 18 | useTheme, 19 | } from '@mui/material'; 20 | import Typography from '@mui/material/Typography'; 21 | import SearchIcon from '@mui/icons-material/Search'; 22 | import AccountCircle from '@mui/icons-material/AccountCircle'; 23 | import MailIcon from '@mui/icons-material/Mail'; 24 | import NotificationsIcon from '@mui/icons-material/Notifications'; 25 | import MoreIcon from '@mui/icons-material/MoreVert'; 26 | import { useSelector } from 'react-redux'; 27 | import AlertBox from './AlertBox'; 28 | //////////////////////hay added for light/dark mode///////////////////// 29 | import { ColorModeContext, tokens } from './stylesheets/Themes'; 30 | import { useContext } from 'react'; 31 | import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined'; 32 | import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'; 33 | import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined'; 34 | import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; 35 | import PersonOutlinedIcon from '@mui/icons-material/PersonOutlined'; 36 | //////////////////////hay added for light/dark mode///////////////////// 37 | 38 | // const theme = createTheme({ 39 | // // typography: { 40 | // // fontFamily: "Roboto", 41 | // // }, 42 | // palette: { 43 | // primary: { 44 | // main: "#222130", 45 | // light: "#2A2A43", 46 | // dark: "#2E2D3D", 47 | // }, 48 | // secondary: { 49 | // main: "#7275F1", 50 | // }, 51 | // }, 52 | // }); 53 | 54 | const Header = () => { 55 | const dispatch = useDispatch(); 56 | //////////////////////hay added for light/dark mode///////////////////// 57 | const theme = useTheme(); 58 | const colors = tokens(theme.palette.mode); 59 | const colorMode = useContext(ColorModeContext); 60 | //////////////////////hay added for light/dark mode///////////////////// 61 | 62 | const [anchorEl, setAnchorEl] = React.useState(null); 63 | 64 | const isMenuOpen = Boolean(anchorEl); 65 | 66 | const handleProfileMenuOpen = (event) => { 67 | setAnchorEl(event.currentTarget); 68 | }; 69 | 70 | const handleMenuClose = () => { 71 | setAnchorEl(null); 72 | }; 73 | 74 | const handleSignOut = () => { 75 | dispatch(logOutActionCreator()) 76 | }; 77 | 78 | const displayAlerts = useSelector((state) => state.alert.displayAlerts); 79 | React.useEffect(() => { 80 | setAlertsCount(displayAlerts.length); 81 | }, [displayAlerts]); 82 | 83 | const [alertsCount, setAlertsCount] = React.useState(0); 84 | 85 | const menuId = 'primary-search-account-menu'; 86 | const renderMenu = ( 87 | 102 | Profile 103 | My account 104 | Sign out 105 | 106 | ); 107 | 108 | const [alertsDrawerToggle, setAlertsDrawerToggle] = React.useState(false); 109 | 110 | const handleAlertsDrawerToggle = () => { 111 | setAlertsDrawerToggle(!alertsDrawerToggle); 112 | }; 113 | 114 | let anomalies = displayAlerts.sort((a, b) => new Date(b.detected_at) - new Date(a.detected_at)); 115 | anomalies = anomalies.map((alertObj, i) => ( 116 | 117 | )); 118 | 119 | const renderAlertsDrawer = ( 120 | 132 | 133 | 134 | 140 | 141 | 142 | 143 | 144 | New alerts 145 | 146 | 147 | {anomalies} 148 | 149 | 150 | 151 | ); 152 | 153 | return ( 154 | // 155 | 156 | 157 | 158 | 159 | {theme.palette.mode === 'dark' ? ( 160 | 161 | ) : ( 162 | 163 | )} 164 | 165 | 171 | 172 | 173 | 174 | 175 | 184 | 185 | 186 | 187 | {renderAlertsDrawer} 188 | {renderMenu} 189 | 190 | // 191 | ); 192 | }; 193 | 194 | export default Header; 195 | -------------------------------------------------------------------------------- /client/components/LandingVisualizer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const LandingVisualizer = () => { 4 | (function () { 5 | "use strict"; 6 | // 'To actually be able to display anything with Three.js, we need three things: 7 | // A scene, a camera, and a renderer so we can render the scene with the camera.' 8 | // - https://threejs.org/docs/#Manual/Introduction/Creating_a_scene 9 | 10 | var scene, camera, renderer; 11 | 12 | // I guess we need this stuff too 13 | var container, 14 | HEIGHT, 15 | WIDTH, 16 | fieldOfView, 17 | aspectRatio, 18 | nearPlane, 19 | farPlane, 20 | stats, 21 | geometry, 22 | particleCount, 23 | i, 24 | h, 25 | color, 26 | size, 27 | materials = [], 28 | mouseX = 0, 29 | mouseY = 0, 30 | windowHalfX, 31 | windowHalfY, 32 | cameraZ, 33 | fogHex, 34 | fogDensity, 35 | parameters = {}, 36 | parameterCount, 37 | particles; 38 | 39 | init(); 40 | animate(); 41 | 42 | function init() { 43 | HEIGHT = window.innerHeight; 44 | WIDTH = window.innerWidth; 45 | windowHalfX = WIDTH / 2; 46 | windowHalfY = HEIGHT / 2; 47 | 48 | fieldOfView = 75; 49 | aspectRatio = WIDTH / HEIGHT; 50 | nearPlane = 1; 51 | farPlane = 3000; 52 | 53 | /* fieldOfView — Camera frustum vertical field of view. 54 | aspectRatio — Camera frustum aspect ratio. 55 | nearPlane — Camera frustum near plane. 56 | farPlane — Camera frustum far plane. 57 | 58 | - https://threejs.org/docs/#Reference/Cameras/PerspectiveCamera 59 | 60 | In geometry, a frustum (plural: frusta or frustums) 61 | is the portion of a solid (normally a cone or pyramid) 62 | that lies between two parallel planes cutting it. - wikipedia. */ 63 | 64 | cameraZ = farPlane / 3; /* So, 1000? Yes! move on! */ 65 | fogHex = 0x000000; /* As black as your heart. */ 66 | fogDensity = 0.0007; /* So not terribly dense? */ 67 | 68 | camera = new THREE.PerspectiveCamera( 69 | fieldOfView, 70 | aspectRatio, 71 | nearPlane, 72 | farPlane 73 | ); 74 | camera.position.z = cameraZ; 75 | 76 | scene = new THREE.Scene(); 77 | scene.fog = new THREE.FogExp2(fogHex, fogDensity); 78 | 79 | container = document.createElement("div"); 80 | document.body.appendChild(container); 81 | document.body.style.margin = 0; 82 | document.body.style.overflow = "hidden"; 83 | 84 | geometry = new THREE.Geometry(); /* NO ONE SAID ANYTHING ABOUT MATH! UGH! */ 85 | 86 | particleCount = 20000; /* Leagues under the sea */ 87 | 88 | /* Hope you took your motion sickness pills; 89 | We're about to get loopy. */ 90 | 91 | for (i = 0; i < particleCount; i++) { 92 | var vertex = new THREE.Vector3(); 93 | vertex.x = Math.random() * 2000 - 1000; 94 | vertex.y = Math.random() * 2000 - 1000; 95 | vertex.z = Math.random() * 2000 - 1000; 96 | 97 | geometry.vertices.push(vertex); 98 | } 99 | 100 | /* We can't stop here, this is bat country! */ 101 | 102 | parameters = [ 103 | [[1, 1, 0.5], 5], 104 | [[0.95, 1, 0.5], 4], 105 | [[0.9, 1, 0.5], 3], 106 | [[0.85, 1, 0.5], 2], 107 | [[0.8, 1, 0.5], 1], 108 | ]; 109 | parameterCount = parameters.length; 110 | 111 | /* I told you to take those motion sickness pills. 112 | Clean that vommit up, we're going again! */ 113 | 114 | for (i = 0; i < parameterCount; i++) { 115 | color = parameters[i][0]; 116 | size = parameters[i][1]; 117 | 118 | materials[i] = new THREE.PointCloudMaterial({ 119 | size: size, 120 | }); 121 | 122 | particles = new THREE.PointCloud(geometry, materials[i]); 123 | 124 | particles.rotation.x = Math.random() * 6; 125 | particles.rotation.y = Math.random() * 6; 126 | particles.rotation.z = Math.random() * 6; 127 | 128 | scene.add(particles); 129 | } 130 | 131 | /* If my calculations are correct, when this baby hits 88 miles per hour... 132 | you're gonna see some serious shit. */ 133 | 134 | renderer = new THREE.WebGLRenderer(); /* Rendererererers particles. */ 135 | renderer.setPixelRatio( 136 | window.devicePixelRatio 137 | ); /* Probably 1; unless you're fancy. */ 138 | renderer.setSize(WIDTH, HEIGHT); /* Full screen baby Wooooo! */ 139 | 140 | container.appendChild( 141 | renderer.domElement 142 | ); /* Let's add all this crazy junk to the page. */ 143 | 144 | /* I don't know about you, but I like to know how bad my 145 | code is wrecking the performance of a user's machine. 146 | Let's see some damn stats! */ 147 | 148 | stats = new Stats(); 149 | stats.domElement.style.position = "absolute"; 150 | stats.domElement.style.top = "0px"; 151 | stats.domElement.style.right = "0px"; 152 | container.appendChild(stats.domElement); 153 | 154 | /* Event Listeners */ 155 | 156 | window.addEventListener("resize", onWindowResize, false); 157 | document.addEventListener("mousemove", onDocumentMouseMove, false); 158 | document.addEventListener("touchstart", onDocumentTouchStart, false); 159 | document.addEventListener("touchmove", onDocumentTouchMove, false); 160 | } 161 | 162 | function animate() { 163 | requestAnimationFrame(animate); 164 | render(); 165 | stats.update(); 166 | } 167 | 168 | function render() { 169 | var time = Date.now() * 0.00005; 170 | 171 | camera.position.x += (mouseX - camera.position.x) * 0.05; 172 | camera.position.y += (-mouseY - camera.position.y) * 0.05; 173 | 174 | camera.lookAt(scene.position); 175 | 176 | for (i = 0; i < scene.children.length; i++) { 177 | var object = scene.children[i]; 178 | 179 | if (object instanceof THREE.PointCloud) { 180 | object.rotation.y = time * (i < 4 ? i + 1 : -(i + 1)); 181 | } 182 | } 183 | 184 | for (i = 0; i < materials.length; i++) { 185 | color = parameters[i][0]; 186 | 187 | h = ((360 * (color[0] + time)) % 360) / 360; 188 | materials[i].color.setHSL(h, color[1], color[2]); 189 | } 190 | 191 | renderer.render(scene, camera); 192 | } 193 | 194 | function onDocumentMouseMove(e) { 195 | mouseX = e.clientX - windowHalfX; 196 | mouseY = e.clientY - windowHalfY; 197 | } 198 | 199 | /* Mobile users? I got your back homey */ 200 | 201 | function onDocumentTouchStart(e) { 202 | if (e.touches.length === 1) { 203 | e.preventDefault(); 204 | mouseX = e.touches[0].pageX - windowHalfX; 205 | mouseY = e.touches[0].pageY - windowHalfY; 206 | } 207 | } 208 | 209 | function onDocumentTouchMove(e) { 210 | if (e.touches.length === 1) { 211 | e.preventDefault(); 212 | mouseX = e.touches[0].pageX - windowHalfX; 213 | mouseY = e.touches[0].pageY - windowHalfY; 214 | } 215 | } 216 | 217 | function onWindowResize() { 218 | windowHalfX = window.innerWidth / 2; 219 | windowHalfY = window.innerHeight / 2; 220 | 221 | camera.aspect = window.innerWidth / window.innerHeight; 222 | camera.updateProjectionMatrix(); 223 | renderer.setSize(window.innerWidth, window.innerHeight); 224 | } 225 | })(); 226 | 227 | export default LandingVisualizer -------------------------------------------------------------------------------- /client/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { logInActionCreator, saveDBActionCreator } from '../actions/actions'; 4 | //mui imports below: 5 | import { 6 | Roboto, 7 | Container, 8 | Switch, 9 | Link, 10 | Box, 11 | TextField, 12 | Typography, 13 | Button, 14 | Tab, 15 | Tabs, 16 | IconButton, 17 | } from '@mui/material'; 18 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 19 | // import { CssBaseline } from '@material-ui/core'; 20 | import CloseIcon from '@mui/icons-material/Close'; 21 | 22 | const theme = createTheme({ 23 | typography: { 24 | fontFamily: ['Pixelify Sans', 'sans-serif'].join(','), 25 | }, 26 | palette: { 27 | primary: { 28 | main: '#766ffc', 29 | }, 30 | secondary: { 31 | main: '#B5B8CB', 32 | }, 33 | }, 34 | components: { 35 | MuiButton: { 36 | styleOverrides: { 37 | root: { 38 | borderRadius: 30, 39 | padding: 15, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }); 45 | 46 | const LoginContainer = ({ closeLogin }) => { 47 | const [regToggle, setRegToggle] = useState(false); 48 | const [selectedTab, setSelectedTab] = useState('login'); 49 | const [user_id, setUser_id] = useState(''); 50 | const [username, setUsername] = useState(''); 51 | const [password, setPassword] = useState(''); 52 | const [uri, setUri] = useState(''); 53 | const [error, setError] = useState(''); 54 | 55 | const dispatch = useDispatch(); 56 | 57 | const handleAuth = async (path) => { 58 | try { 59 | console.log( 60 | 'username: ', 61 | username, 62 | 'password: ', 63 | password, 64 | 'uri: ', 65 | uri, 66 | 'path: ', 67 | path 68 | ); 69 | // console.log(`**************** this is your path: ${path} ****************`) 70 | const requestOptions = { 71 | method: 'POST', 72 | headers: { 'Content-Type': 'application/json' }, 73 | body: JSON.stringify({ username, password, uri }), 74 | }; 75 | const response = await fetch(path, requestOptions); 76 | const data = await response.json(); 77 | 78 | console.log('data returned in handleAuth func in LoginContainer', data); 79 | if (!response.ok) throw new Error(data.error || 'Error from server'); 80 | 81 | console.log( 82 | `user_id: ${data.user_id}, username: ${data.username}, uri: ${data.uri}` 83 | ); 84 | console.log( 85 | 'dbArray in handleAuth func in LoginContainer: ', 86 | data.dbArray 87 | ); 88 | dispatch(logInActionCreator(data.user_id, data.username, data.uri)); 89 | // dispatch(saveDBActionCreator(data.dbArray)); 90 | 91 | //leaving this open for now, but here is where we will go store shit in redux state 92 | if (!response.ok) { 93 | setError(data.error || 'Error from server'); 94 | throw new Error(data.error || 'Error from server'); 95 | } 96 | } catch (err) { 97 | console.error( 98 | 'error caught in handleAuth in LoginContainer', 99 | err.message 100 | ); 101 | } 102 | }; 103 | const handleChange = (event, newValue) => { 104 | setRegToggle(!regToggle); 105 | setSelectedTab(newValue); 106 | }; 107 | 108 | return ( 109 | 110 | 111 | 136 | {/*
*/} 137 | 144 | 145 | 146 | {/*
*/} 147 | 148 | Sign In 149 | 150 | 153 | regToggle ? handleAuth('/register') : handleAuth('/login') 154 | } 155 | noValidate 156 | sx={{ mt: 1 }} 157 | > 158 | {/* */} 166 | 167 | 168 | 169 | 170 | setUsername(e.target.value)} 178 | autoFocus 179 | sx={{ 180 | input: { color: '#6870fa' }, 181 | // backgroundColor: "#3B3B4B", 182 | backgroundColor: '#e0e0e0', 183 | borderRadius: '5px', 184 | }} 185 | InputLabelProps={{ 186 | style: { color: '#6870fa' }, 187 | }} 188 | InputProps={{ 189 | style: { color: '#6870fa' }, 190 | }} 191 | /> 192 | 193 | {/* {error} username does not exist */} 194 | 195 | setPassword(e.target.value)} 204 | sx={{ 205 | input: { color: '#6870fa' }, 206 | // backgroundColor: "#3B3B4B", 207 | backgroundColor: '#e0e0e0', 208 | borderRadius: '5px', 209 | }} 210 | InputLabelProps={{ 211 | style: { color: '#6870fa' }, 212 | }} 213 | InputProps={{ 214 | style: { color: '#6870fa' }, 215 | }} 216 | /> 217 | {selectedTab === 'register' && ( 218 | setUri(e.target.value)} 226 | sx={{ 227 | input: { color: '#6870fa' }, 228 | // backgroundColor: "#3B3B4B", 229 | backgroundColor: '#e0e0e0', 230 | borderRadius: '5px', 231 | }} 232 | InputLabelProps={{ 233 | style: { color: '#6870fa' }, 234 | }} 235 | InputProps={{ 236 | style: { color: '#6870fa' }, 237 | }} 238 | /> 239 | )} 240 | 258 | 259 |
260 |
261 |
262 | ); 263 | }; 264 | 265 | export default LoginContainer; 266 | -------------------------------------------------------------------------------- /client/components/MonitorHeader.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Typography, 3 | Box, 4 | useTheme, 5 | Select, 6 | FormControl, 7 | InputLabel, 8 | MenuItem, 9 | Chip, 10 | } from '@mui/material'; 11 | import tokens from './stylesheets/Themes'; 12 | import React, { useState, useEffect } from 'react'; 13 | import { useSelector, useDispatch } from 'react-redux'; 14 | import { displayMonitorsActionCreator } from '../actions/actions'; 15 | 16 | const SubHeader = ({ title, subtitle }) => { 17 | const theme = useTheme(); 18 | const colors = tokens(theme.palette.mode); 19 | return ( 20 | 21 | 27 | {title} 28 | 29 | 30 | {/* {subtitle} */} 31 | 32 | 33 | ); 34 | }; 35 | 36 | const MonitorHeader = () => { 37 | const dispatch = useDispatch(); 38 | const monitors = useSelector((state) => state.monitor.activeMonitors); 39 | const displayMonitors = useSelector((state) => state.monitor.displayMonitors); 40 | 41 | const [selectedTable, setSelectedTable] = useState(''); 42 | const [selectedColumn, setSelectedColumn] = useState(''); 43 | const [selectedMonitorType, setSelectedMonitorType] = useState(''); 44 | 45 | // look at monitors 46 | // pull out the tables that are being monitored 47 | const tablesWithMonitors = []; 48 | monitors.forEach((monitor) => { 49 | if (!tablesWithMonitors.includes(monitor.parameters.table)) { 50 | tablesWithMonitors.push(monitor.parameters.table); 51 | } 52 | }); 53 | 54 | // pull out the columns that are being monitored, tied to the table 55 | const columnsOnTablesWithMonitors = {}; 56 | tablesWithMonitors.forEach((table) => { 57 | columnsOnTablesWithMonitors[table] = []; 58 | }); 59 | monitors.forEach((monitor) => { 60 | if ( 61 | columnsOnTablesWithMonitors[monitor.parameters.table] && 62 | !columnsOnTablesWithMonitors[monitor.parameters.table].includes( 63 | monitor.parameters.column 64 | ) 65 | ) { 66 | columnsOnTablesWithMonitors[monitor.parameters.table].push( 67 | monitor.parameters.column 68 | ); 69 | } 70 | }); 71 | 72 | // pull out the monitor types that are in monitors 73 | const monitorTypesInUse = []; 74 | monitors.forEach((monitor) => { 75 | if (!monitorTypesInUse.includes(monitor.type)) { 76 | monitorTypesInUse.push(monitor.type); 77 | } 78 | }); 79 | 80 | console.log('tablesWithMonitors', tablesWithMonitors); 81 | console.log('columnsOnTablesWithMonitors', columnsOnTablesWithMonitors); 82 | 83 | useEffect(() => { 84 | const filteredMonitors = monitors.filter((monitor) => { 85 | if (selectedTable && monitor.parameters.table !== selectedTable) { 86 | return false; 87 | } 88 | if (selectedColumn && monitor.parameters.column !== selectedColumn) { 89 | return false; 90 | } 91 | if (selectedMonitorType && monitor.type !== selectedMonitorType) { 92 | return false; 93 | } 94 | return true; 95 | }); 96 | dispatch(displayMonitorsActionCreator(filteredMonitors)); 97 | }, [selectedTable, selectedColumn, selectedMonitorType, monitors]); 98 | 99 | // dropdown for tables 100 | // dropdown for columns on tables 101 | // dropdown for monitor types 102 | // chip for reset filters 103 | 104 | return ( 105 |
106 | 107 | 111 | 112 | 113 | 114 | Table Name 115 | 135 | 136 | 137 | Column Name 138 | 158 | 159 | 160 | Monitor Type 161 | 180 | 181 | { 184 | setSelectedTable(''); 185 | setSelectedColumn(''); 186 | setSelectedMonitorType(''); 187 | }} 188 | size='large' 189 | sx={{ 190 | fontSize: '1.2em', 191 | mt: 1, 192 | }} 193 | /> 194 | 195 | 196 |
197 | ); 198 | }; 199 | 200 | export default MonitorHeader; 201 | -------------------------------------------------------------------------------- /client/components/NodeStyle.jsx: -------------------------------------------------------------------------------- 1 | const CustomNode = ({ data }) => { 2 | return ( 3 |
4 | 5 |

{data.label}

6 |
    7 | {data.columns.map((column, index) => ( 8 |
  • {column}
  • 9 | ))} 10 |
11 | 12 |
13 | ); 14 | } -------------------------------------------------------------------------------- /client/components/QueryHeader.jsx: -------------------------------------------------------------------------------- 1 | import { Typography, Box, useTheme } from "@mui/material"; 2 | import tokens from "./stylesheets/Themes"; 3 | import React from "react"; 4 | 5 | const SubHeader = ({ title, subtitle }) => { 6 | const theme = useTheme(); 7 | const colors = tokens(theme.palette.mode); 8 | return ( 9 | 10 | 16 | {title} 17 | 18 | 19 | {/* {subtitle} */} 20 | 21 | 22 | ); 23 | }; 24 | 25 | const QueryHeader = () => { 26 | return ( 27 |
28 | 29 | 33 | 34 |
35 | ); 36 | }; 37 | 38 | export default QueryHeader; 39 | -------------------------------------------------------------------------------- /client/components/SideBar.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useState } from 'react'; 3 | import Box from '@mui/material/Box'; 4 | import Drawer from '@mui/material/Drawer'; 5 | import Toolbar from '@mui/material/Toolbar'; 6 | import List from '@mui/material/List'; 7 | import Typography from '@mui/material/Typography'; 8 | import Divider from '@mui/material/Divider'; 9 | import ListItem from '@mui/material/ListItem'; 10 | import ListItemButton from '@mui/material/ListItemButton'; 11 | import ListItemText from '@mui/material/ListItemText'; 12 | import InboxIcon from '@mui/icons-material/MoveToInbox'; 13 | import MailIcon from '@mui/icons-material/Mail'; 14 | import { 15 | AppBar, 16 | createTheme, 17 | CssBaseline, 18 | ThemeProvider, 19 | ListItemIcon, 20 | useTheme, 21 | IconButton, 22 | Button, 23 | } from '@mui/material'; 24 | import { useDispatch } from 'react-redux'; 25 | import { selectPageActionCreator } from '../actions/actions'; 26 | 27 | //icons 28 | //dashboard 29 | import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'; 30 | import HomeIcon from '@mui/icons-material/Home'; 31 | //alerts 32 | import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined'; 33 | import { NearbyErrorOutlined } from '@mui/icons-material'; 34 | //ERD 35 | import AccountTreeIcon from '@mui/icons-material/AccountTree'; 36 | import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined'; 37 | //monitor Outlined 38 | import InsightsIcon from '@mui/icons-material/Insights'; 39 | import AutoGraphOutlinedIcon from '@mui/icons-material/AutoGraphOutlined'; 40 | import AutoGraphIcon from '@mui/icons-material/AutoGraph'; 41 | //reports 42 | import MonitorHeartIcon from '@mui/icons-material/MonitorHeart'; 43 | import AssessmentOutlinedIcon from '@mui/icons-material/AssessmentOutlined'; 44 | import AssessmentIcon from '@mui/icons-material/Assessment'; 45 | //query 46 | import QueryStatsOutlinedIcon from '@mui/icons-material/QueryStatsOutlined'; 47 | import QueryStatsIcon from '@mui/icons-material/QueryStats'; 48 | //light/dark mode 49 | import LightModeIcon from '@mui/icons-material/LightMode'; 50 | //settings 51 | import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined'; 52 | import SettingsIcon from '@mui/icons-material/Settings'; 53 | //logo 54 | import GradeIcon from '@mui/icons-material/Grade'; 55 | import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; 56 | import CastleIcon from '@mui/icons-material/Castle'; 57 | import BubbleChartIcon from '@mui/icons-material/BubbleChart'; 58 | 59 | //////////////////////hay added for light/dark mode///////////////////// 60 | import tokens from './stylesheets/Themes'; 61 | import { ProSidebar, Menu, MenuItem } from 'react-pro-sidebar'; 62 | 63 | const Item = ({ title, to, icon, selected, setSelected }) => { 64 | const theme = useTheme(); 65 | const colors = tokens(theme.palette.mode); 66 | return ( 67 | setSelected(title)} 73 | icon={icon} 74 | > 75 | {title} 76 | 77 | 78 | ); 79 | }; 80 | 81 | //////////////////////END light/dark mode///////////////////// 82 | 83 | // const theme = createTheme({ 84 | // palette: { 85 | // primary: { 86 | // main: "#2F3243", 87 | // light: "#E5E7FA", 88 | // dark: "#1565c0", 89 | // }, 90 | // secondary: { 91 | // main: "#E5E7FA", 92 | // }, 93 | // }, 94 | // }); 95 | 96 | const listItems = [ 97 | { 98 | listIcon: , 99 | listText: 'Dashboard', 100 | }, 101 | { 102 | listIcon: , 103 | listText: 'ERD', 104 | }, 105 | { 106 | listIcon: , 107 | listText: 'Monitors', 108 | }, 109 | { 110 | listIcon: , 111 | listText: 'Alerts', 112 | }, 113 | // { 114 | // listIcon: , 115 | // listText: "Query", 116 | // }, 117 | ]; 118 | 119 | const hackLogo = { 120 | listIcon: , 121 | listText: 'Query', 122 | }; 123 | 124 | const drawerWidth = 120; 125 | 126 | const SideBar = () => { 127 | //////////////////////hay added for light/dark mode///////////////////// 128 | const theme = useTheme(); 129 | const colors = tokens(theme.palette.mode); 130 | // const [isCollapsed, setIsCollapsed] = useState(false); 131 | const [selectedItem, setSelectedItem] = useState(null); 132 | const handleClick = (item) => { 133 | setSelectedItem(item); 134 | dispatch(selectPageActionCreator(item.listText)); 135 | }; 136 | //////////////////////hay added for light/dark mode///////////////////// 137 | 138 | const dispatch = useDispatch(); 139 | 140 | return ( 141 |
142 | {/* */} 143 | 157 | 165 | 166 | 167 | 176 | MoniQL 177 | 178 | 179 | 180 | 181 | 182 | 183 | {listItems.map((item, index) => ( 184 | 190 | handleClick(item)} 198 | > 199 | 204 | 215 | {item.listIcon} 216 | 217 | 224 | {item.listText} 225 | 226 | } 227 | /> 228 | 229 | 230 | 231 | ))} 232 | 233 | 234 | 235 | 244 | 245 | 246 | 247 | 248 | 249 | {/* */} 250 |
251 | ); 252 | }; 253 | 254 | export default SideBar; 255 | -------------------------------------------------------------------------------- /client/components/SubHeader.jsx: -------------------------------------------------------------------------------- 1 | import { Typography, Box, useTheme } from "@mui/material"; 2 | import tokens from "./stylesheets/Themes"; 3 | import React from "react"; 4 | 5 | const SubHeader = ({ title, subtitle }) => { 6 | const theme = useTheme(); 7 | const colors = tokens(theme.palette.mode); 8 | return ( 9 | 10 | 16 | {title} 17 | 18 | 19 | {subtitle} 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default SubHeader; 26 | -------------------------------------------------------------------------------- /client/components/monitors/CustomMonitor.jsx: -------------------------------------------------------------------------------- 1 | // React imports 2 | import React, { useEffect, useState } from 'react'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | 5 | // Action creators 6 | import { addMonitorsActionCreator } from '../../actions/actions'; 7 | 8 | 9 | // MUI components 10 | import { 11 | Box, 12 | Card, 13 | Button, 14 | Divider, 15 | FormControl, 16 | FormHelperText, 17 | Stack, 18 | Typography, 19 | TextField, 20 | useTheme 21 | } from '@mui/material'; 22 | import tokens from '../stylesheets/Themes'; 23 | 24 | // MUI icons 25 | import monitorObjectCreator from './monitorObjectCreator'; 26 | 27 | 28 | const CustomMonitor = () => { 29 | const dispatch = useDispatch(); 30 | const [params, setParams] = useState({ 31 | table: 'custom query table(s)', 32 | frequency: '', 33 | description: '' 34 | }); 35 | //color themes 36 | const theme = useTheme(); 37 | const colors = tokens(theme.palette.mode); 38 | 39 | const tablesArray = useSelector((state) => state.diagram.data); 40 | const user_id = useSelector((state) => state.user.user_id); 41 | const [columnsArray, setColumnsArray] = useState([]); 42 | 43 | //for editing monitors with existing rules 44 | const handleChanges = (e) => { 45 | // console.log('THIS IS THE NAME OF THE FIELD',e.target.name, 'THIS IS THE VALUE THE USER CHOSE', e.target.value) 46 | setParams({ ...params, [e.target.name]: e.target.value }); 47 | } 48 | 49 | const handleSubmit = async (e) => { 50 | e.preventDefault(); 51 | console.log('params in customMonitor', params); 52 | const monitorObject = monitorObjectCreator('Custom', user_id, params); 53 | 54 | //make post request to server 55 | try { 56 | const response = await fetch('/monitors', { 57 | method: 'POST', 58 | headers: { 59 | 'Content-Type': 'application/json', 60 | }, 61 | body: JSON.stringify(monitorObject), 62 | }); 63 | if (!response.ok) { 64 | throw new Error(`HTTP error! status: ${response.status}`); 65 | } 66 | const data = await response.json(); 67 | console.log(data); 68 | 69 | dispatch(addMonitorsActionCreator(data)); 70 | } catch (error) { 71 | console.log('fetch error: ', error); 72 | } 73 | } 74 | 75 | return ( 76 |
77 | 78 | 92 | 93 | 94 | 95 | Create New Custom Monitor 96 | 97 | 98 | 99 | 105 | {/* Frequency Input */} 106 | 121 | 122 | Enter the frequency (in hours) for the monitor to run 123 | 124 | 125 | {/* Custom Query Input */} 126 | 141 | 142 | Enter your custom query string to monitor (Structure your query 143 | so that it will only return anomalous rows) 144 | 145 | 146 | {/* Description Input */} 147 | 163 | 164 | Enter a description for the monitor 165 | 166 | 167 | 179 | 180 | 181 | 182 |
183 | ); 184 | } 185 | 186 | export default CustomMonitor; 187 | -------------------------------------------------------------------------------- /client/components/monitors/FreshnessMonitor.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { addMonitorsActionCreator } from '../../actions/actions'; 4 | import { 5 | Box, 6 | Card, 7 | Button, 8 | Divider, 9 | InputLabel, 10 | FormControl, 11 | FormHelperText, 12 | Stack, 13 | Typography, 14 | MenuItem, 15 | Select, 16 | TextField, 17 | } from '@mui/material'; 18 | import AddCircleIcon from '@mui/icons-material/AddCircle'; 19 | import monitorObjectCreator from './monitorObjectCreator'; 20 | 21 | const FreshnessMonitor = () => { 22 | const dispatch = useDispatch(); 23 | const [params, setParams] = useState({ 24 | table: '', 25 | frequency: '', 26 | description: '', 27 | howLongIsTooLong: '', 28 | }); 29 | 30 | const tablesArray = useSelector((state) => state.diagram.data); 31 | const user_id = useSelector((state) => state.user.user_id); 32 | const [columnsArray, setColumnsArray] = useState([]); 33 | 34 | //for editing monitors with existing rules 35 | const handleChanges = (e) => { 36 | // console.log('THIS IS THE NAME OF THE DROPDOWNLIST: ', e.target.name, 'THIS IS THE VALUE THE USER CHOSE: ', e.target.value) 37 | setParams({ ...params, [e.target.name]: e.target.value }); 38 | }; 39 | 40 | const handleSubmit = async (e) => { 41 | e.preventDefault(); 42 | console.log('params in freshness monitor handlesubmit', params); 43 | const monitorObject = monitorObjectCreator('Freshness', user_id, params); 44 | 45 | //make post request to server 46 | try { 47 | const response = await fetch('/monitors', { 48 | method: 'POST', 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | body: JSON.stringify(monitorObject), 53 | }); 54 | if (!response.ok) { 55 | throw new Error(`HTTP error! status: ${response.status}`); 56 | } 57 | const data = await response.json(); 58 | console.log(data); 59 | 60 | dispatch(addMonitorsActionCreator(data)); 61 | } catch (error) { 62 | console.log('fetch error:', error); 63 | } 64 | }; 65 | 66 | return ( 67 |
68 | 69 | 82 | 83 | 84 | 85 | New Freshness Monitor 86 | 87 | 88 | 89 | 95 | {/* TABLE SELECT */} 96 | 97 | 98 | Table Name 99 | 100 | 101 | 121 | Select table to monitor 122 | 123 | 124 | {/* Frequency Input */} 125 | 126 | 140 | 141 | Enter the frequency (in hours) for the monitor to run 142 | 143 | 144 | {/* Define Anomalous Input */} 145 | 159 | 160 | After how many hours with no updates would you like to fire an 161 | alert? 162 | 163 | 164 | {/* Description Input */} 165 | 180 | 181 | Enter a description for the monitor 182 | 183 | 184 | 195 | 196 | 197 | 198 |
199 | ); 200 | }; 201 | 202 | export default FreshnessMonitor; 203 | -------------------------------------------------------------------------------- /client/components/monitors/MonitorEditor.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { updateMonitorActionCreator } from '../../actions/actions'; 4 | 5 | function MonitorEditor({ monitor, onDone }) { 6 | const dispatch = useDispatch(); 7 | const [params, setParams] = useState(monitor.parameters); 8 | 9 | const handleChanges = (e) => { 10 | setParams({ 11 | ...params, 12 | [e.target.name]: e.target.value, 13 | }); 14 | }; 15 | 16 | const handleSubmit = () => { 17 | onDone({ 18 | ...monitor, 19 | parameters: params, 20 | }); 21 | }; 22 | 23 | return ( 24 |
25 | {Object.entries(params).map(([key, value]) => ( 26 |
27 | 28 | 29 |
30 | ))} 31 | 32 |
33 | ); 34 | } 35 | 36 | export default MonitorEditor; 37 | -------------------------------------------------------------------------------- /client/components/monitors/NullMonitor.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { addMonitorsActionCreator } from '../../actions/actions'; 4 | import { 5 | Box, 6 | Card, 7 | Button, 8 | Divider, 9 | InputLabel, 10 | FormControl, 11 | FormHelperText, 12 | Stack, 13 | Typography, 14 | MenuItem, 15 | Select, 16 | TextField, 17 | } from '@mui/material'; 18 | import AddCircleIcon from '@mui/icons-material/AddCircle'; 19 | import monitorObjectCreator from './monitorObjectCreator'; 20 | 21 | 22 | const NullMonitor = () => { 23 | const dispatch = useDispatch(); 24 | const [params, setParams] = useState({ 25 | table: '', 26 | frequency: '', 27 | description: '' 28 | }); 29 | 30 | const tablesArray = useSelector((state) => state.diagram.data); 31 | const user_id = useSelector((state) => state.user.user_id); 32 | 33 | const [columnsArray, setColumnsArray] = useState([]); 34 | 35 | //for editing monitors with existing rules 36 | const handleChanges = (e) => { 37 | // console.log('THIS IS THE NAME OF THE DROPDOWNLIST',e.target.name, 'THIS IS THE VALUE THE USER CHOSE', e.target.value) 38 | setParams({ ...params, [e.target.name]: e.target.value }); 39 | } 40 | 41 | 42 | const addMonitor = async (e) => { 43 | e.preventDefault(); 44 | // console.log("this is params in nullMonitor", params); 45 | // const monitorObject = { type: "null", params: JSON.stringify(params) }; 46 | const monitorObject = monitorObjectCreator('Null', user_id, params); 47 | try { 48 | const response = await fetch('/monitors', { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json' 52 | }, 53 | body: JSON.stringify(monitorObject) 54 | }) 55 | if (!response.ok) { 56 | throw new Error (`HTTP error! status: ${response.status}`); 57 | } 58 | const data = await response.json(); 59 | 60 | // console.log('data returned in addMonitor in null monitor component: ', data); 61 | // console.log('Data Parameters',data[0].parameters); 62 | 63 | dispatch(addMonitorsActionCreator(data)) 64 | 65 | } catch (error) { 66 | console.log('fetch error:', error); 67 | } 68 | } 69 | 70 | 71 | 72 | return ( 73 |
74 | 75 | 87 | 88 | 89 | 90 | New Null Monitor 91 | 92 | 93 | 94 | 95 | 96 | 97 | Table Name 98 | 99 | {/* TABLE SELECT */} 100 | 119 | Select table to monitor 120 | 121 | {/* Frequency Input */} 122 | 123 | 137 | 138 | Enter the frequency (in hours) for the monitor to run 139 | 140 | 141 | 142 | {/* Description Input */} 143 | 158 | Enter a description for the monitor 159 | 160 | 171 | 172 | 173 | 174 |
175 | ); 176 | } 177 | 178 | export default NullMonitor; 179 | 180 | 181 | 182 | 183 | 184 | // return ( 185 | //
186 | // 187 | // 200 | // 201 | // 202 | // 203 | // Create New Null Monitor 204 | // 205 | // 206 | // 207 | // 213 | // {/* TABLE SELECT */} 214 | // 232 | // Select table to monitor 233 | 234 | // {/* Frequency Input */} 235 | // 256 | // 257 | // Enter the frequency (in hours) for the monitor to run 258 | // 259 | 260 | // {/* Description Input */} 261 | // 283 | // Enter a description for the monitor 284 | // 285 | // 295 | // 296 | // 297 | // 298 | //
299 | // ); 300 | // } 301 | 302 | // export default NullMonitor; -------------------------------------------------------------------------------- /client/components/monitors/VolumeMonitor.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { addMonitorsActionCreator } from "../../actions/actions"; 4 | import { 5 | Box, 6 | Card, 7 | Button, 8 | Divider, 9 | FormControl, 10 | FormHelperText, 11 | Typography, 12 | MenuItem, 13 | Select, 14 | TextField, 15 | InputLabel, 16 | } from "@mui/material"; 17 | import monitorObjectCreator from "./monitorObjectCreator"; 18 | 19 | const VolumeMonitor = () => { 20 | const dispatch = useDispatch(); 21 | const [params, setParams] = useState({ 22 | table: "", 23 | timeColumn: "", 24 | interval: "", 25 | period: "", 26 | ending: "", 27 | }); 28 | 29 | const tablesArray = useSelector((state) => state.diagram.data); 30 | const user_id = useSelector((state) => state.user.user_id); 31 | const [columnsArray, setColumnsArray] = useState([]); 32 | 33 | useEffect(() => { 34 | tablesArray.forEach((table) => { 35 | if (params.table === table.table_name) { 36 | const columns = table.columns.map((column) => column.name); 37 | setColumnsArray(columns); 38 | } 39 | }); 40 | }, [params.table, tablesArray]); 41 | 42 | const handleChanges = (e) => { 43 | setParams({ ...params, [e.target.name]: e.target.value }); 44 | }; 45 | 46 | const handleSubmit = async (e) => { 47 | e.preventDefault(); 48 | console.log("this is params", params); 49 | const monitorObject = monitorObjectCreator("Volume", user_id, params); 50 | // dispatch(addMonitorActionCreator(monitorObject)) 51 | //make post request to server 52 | try { 53 | const response = await fetch("/monitors", { 54 | method: "POST", 55 | headers: { 56 | "Content-Type": "application/json", 57 | }, 58 | body: JSON.stringify(monitorObject), 59 | }); 60 | if (!response.ok) { 61 | throw new Error(`HTTP error! status: ${response.status}`); 62 | } 63 | const data = await response.json(); 64 | console.log(data); 65 | 66 | dispatch(addMonitorsActionCreator(data)); 67 | } catch (error) { 68 | console.log("fetch error:", error); 69 | } 70 | }; 71 | 72 | return ( 73 |
74 | 75 | 88 | 89 | 90 | 91 | New Volume Monitor 92 | 93 | 94 | 95 | 96 | {/* TABLE AND TIME COLUMN SELECT */} 97 | 98 | 99 | 100 | Table Name 101 | 102 | 103 | 123 | Select a table for monitoring 124 | 125 | 126 | 127 | Column Name 128 | 129 | 148 | {/* is this correct? */} 149 | Select column to monitor 150 | 151 | 152 | {/* INTERVAL AND PERIOD AND ENDING INPUT */} 153 | 159 | 160 | 171 | monitoring interval 172 | 173 | 174 | 185 | monitoring period 186 | 187 | 188 | 199 | Select end point 200 | 201 | 202 | {/* Description Input */} 203 | 218 | Enter a description for the monitor 219 | 230 | 231 | 232 | 233 |
234 | ); 235 | }; 236 | 237 | export default VolumeMonitor; 238 | -------------------------------------------------------------------------------- /client/components/monitors/monitorObjectCreator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * monitor object shape: 3 | * { 4 | * type: string, 5 | * user_id: number, 6 | * params: object, 7 | * } 8 | */ 9 | 10 | const monitorObjectCreator = (type, user_id, params) => { 11 | return { 12 | type: type, 13 | user_id: user_id, 14 | parameters: params, 15 | } 16 | }; 17 | 18 | export default monitorObjectCreator; -------------------------------------------------------------------------------- /client/components/stylesheets/App.css: -------------------------------------------------------------------------------- 1 | /* body { 2 | background-color: #222130; 3 | margin: 0; 4 | padding: 0; 5 | overflow-x: hidden; 6 | cursor: url('https://cur.cursors-4u.net/cursors/cur-2/cur113.cur'), auto; 7 | font-family: sans-serif; 8 | } */ 9 | 10 | 11 | @import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:wght@400;600;700&display=swap"); 12 | 13 | html, 14 | body, 15 | #root, 16 | .app, 17 | .content { 18 | height: 100%; 19 | width: 100%; 20 | font-family: "Source Sans Pro", sans-serif; 21 | /* background-color: #222130; */ 22 | /* background-color: #1d1c25; */ 23 | /* background-image: url(https://img.freepik.com/free-vector/blurred-abstract-background-design_1107-169.jpg?w=2000&t=st=1706200718~exp=1706201318~hmac=6998d8adb64a2ea68c5f75f354698c93c53991a6bd012012fa955b8c73718976); */ 24 | /* background-image: url(https://i.pinimg.com/originals/60/15/9f/60159f45955be086e295dd9400ceee86.jpg); */ 25 | 26 | 27 | /* opacity: 0.75; */ 28 | background-repeat: no-repeat; 29 | background-size: cover; 30 | 31 | margin: 0; 32 | padding: 0; 33 | overflow-x: hidden; 34 | overflow-y: auto; 35 | /* background: rgb(33,30,38); 36 | background: radial-gradient(circle, rgba(33,30,38,1) 0%, rgba(44,44,60,1) 100%); */ 37 | background: #211e26; 38 | } 39 | body:before { 40 | content: ""; 41 | position: absolute; 42 | top: 0px; 43 | right: 0px; 44 | bottom: 0px; 45 | left: 0px; 46 | /* background-color: rgba(0,0,0,0.25); */ 47 | z-index: -1; 48 | background-color: transparent; 49 | filter: blur(5px); 50 | opacity: 0.5; 51 | } 52 | .app { 53 | display: flex; 54 | position: relative; 55 | } 56 | 57 | .card-container { 58 | background: rgb(33,30,38); 59 | background: radial-gradient(circle, rgba(33,30,38,1) 0%, rgba(44,44,60,1) 100%); 60 | } 61 | .react-flow__node { 62 | /* filter: blur(1px); */ 63 | font-size: 14px; 64 | text-align: left; 65 | border: 1px solid #e0e0e0; 66 | /* background-color: #6870fa; */ 67 | z-index: 2; 68 | /* padding: 10px 20px; */ 69 | width: 150px; 70 | height: 200px; 71 | border-radius: 10px; 72 | overflow: scroll; 73 | color: #e0e0e0; 74 | overflow: hidden; 75 | /* background: rgba( 255, 255, 255, 0.25 ); */ 76 | background: rgba(255, 255, 255, 0.1); 77 | backdrop-filter: blur( 10px ); 78 | -webkit-backdrop-filter: blur( 4x ); 79 | border-radius: 10px; 80 | border: 1px solid rgba( 255, 255, 255, 0.18 ); 81 | /* box-shadow: 0px 0px 23px 8px rgba(227,214,255,0.08); */ 82 | /* box-shadow: 83 | inset 0 0 50px #fff, 84 | inset 20px 0 80px #f0f, 85 | inset -20px 0 80px #0ff, 86 | inset 20px 0 300px #f0f, 87 | inset -20px 0 300px #0ff, 88 | 0 0 20px #fff, 89 | -5px 0 30px #f0f, 90 | 5px 0 30px #0ff; */ 91 | } 92 | .react-flow__edge { 93 | /* filter: blur(1px); */ 94 | color: #ffffff; 95 | /* z-index: 1; */ 96 | } 97 | .scrollable-content { 98 | overflow-y: auto; 99 | height: 100%; 100 | padding-right: 15px; 101 | } 102 | 103 | .scrollable-content::-webkit-scrollbar { 104 | width: 8px; 105 | } 106 | 107 | .scrollable-content::-webkit-scrollbar-thumb { 108 | background: rgba(104, 112, 250, 0.5); 109 | border-radius: 10px; 110 | transition: background 0.1s ease-in-out; 111 | } 112 | 113 | .react-flow__node h3 { 114 | text-align: center; 115 | margin-top: .3rem; 116 | margin-bottom: 1rem; 117 | font-size: 16px; 118 | /* color: #ff1673; */ 119 | color: white; 120 | /* text-shadow: 1px 1px 5px #41373b; */ 121 | border-bottom: .3px solid #e0e0e0bb; 122 | line-height: 2.1rem; 123 | } 124 | .react-flow__node ul { 125 | margin-top: -.3rem; 126 | } 127 | 128 | .react-flow__node:hover { 129 | filter: blur(0px); 130 | } 131 | 132 | .react-flow__handle { 133 | background-color: #e0e0e0; 134 | color: #e0e0e0; 135 | /* border: 1px solid black; */ 136 | border-radius: 10px; 137 | width: 10px; 138 | height: 10px; 139 | z-index: 2; 140 | } 141 | 142 | 143 | 144 | ::-webkit-scrollbar { 145 | width: 8px; 146 | border-radius: 10px; 147 | 148 | } 149 | 150 | 151 | 152 | 153 | /* Handle */ 154 | ::-webkit-scrollbar-thumb { 155 | background: rgba(104, 112, 250, 0.01); 156 | border-radius: 10px; 157 | transition: background 0.1s ease-in-out; 158 | } 159 | 160 | 161 | ::-webkit-scrollbar-thumb:active, 162 | ::-webkit-scrollbar-thumb:hover { 163 | background: #6870fa; 164 | } 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | /* 173 | grey { 174 | color: #e0e0e0; 175 | color: #c2c2c2; 176 | color: #a3a3a3; 177 | color: #858585; 178 | color: #666666; 179 | color: #525252; 180 | color: #3d3d3d; 181 | color: #292929; 182 | color: #141414; 183 | color: #26262b; 184 | 185 | } 186 | primary { 187 | color: #d0d1d5; 188 | color: #a1a4ab; 189 | color: #727681; 190 | color: #1F2A40; 191 | color: #141b2d; 192 | color: #101624; 193 | color: #0c101b; 194 | color: #080b12; 195 | color: #040509; 196 | } 197 | greenAccent { 198 | color: #dbf5ee; 199 | color: #b7ebde; 200 | color: #94e2cd; 201 | color: #70d8bd; 202 | color: #4cceac; 203 | color: #3da58a; 204 | color: #2e7c67; 205 | color: #1e5245; 206 | color: #0f2922; 207 | } 208 | redAccent { 209 | color: #f8dcdb; 210 | color: #f1b9b7; 211 | color: #e99592; 212 | color: #e2726e; 213 | color: #db4f4a; 214 | color: #af3f3b; 215 | color: #832f2c; 216 | color: #58201e; 217 | color: #2c100f; 218 | } 219 | blueAccent { 220 | color: #e1e2fe; 221 | color: #c3c6fd; 222 | color: #a4a9fc; 223 | color: #868dfb; 224 | color: #6870fa; 225 | color: #535ac8; 226 | color: #3e4396; 227 | color: #2a2d64; 228 | color: #151632; 229 | } 230 | 231 | grey { 232 | color: #141414; 233 | color: #292929; 234 | color: #3d3d3d; 235 | color: #525252; 236 | color: #666666; 237 | color: #858585; 238 | color: #a3a3a3; 239 | color: #c2c2c2; 240 | color: #e0e0e0; 241 | } 242 | primary { 243 | color: #040509; 244 | color: #080b12; 245 | color: #0c101b; 246 | color: #f2f0f0; 247 | color: #141b2d; 248 | color: #1F2A40; 249 | color: #727681; 250 | color: #a1a4ab; 251 | color: #d0d1d5; 252 | } 253 | greenAccent { 254 | color: #0f2922; 255 | color: #1e5245; 256 | color: #2e7c67; 257 | color: #3da58a; 258 | color: #4cceac; 259 | color: #70d8bd; 260 | color: #94e2cd; 261 | color: #b7ebde; 262 | color: #dbf5ee; 263 | } 264 | redAccent { 265 | color: #2c100f; 266 | color: #58201e; 267 | color: #832f2c; 268 | color: #af3f3b; 269 | color: #db4f4a; 270 | color: #e2726e; 271 | color: #e99592; 272 | color: #f1b9b7; 273 | color: #f8dcdb; 274 | } 275 | blueAccent { 276 | color: #151632; 277 | color: #2a2d64; 278 | color: #3e4396; 279 | color: #535ac8; 280 | color: #6870fa; 281 | color: #868dfb; 282 | color: #a4a9fc; 283 | color: #c3c6fd; 284 | color: #e1e2fe; 285 | } */ 286 | -------------------------------------------------------------------------------- /client/components/stylesheets/Themes.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useMemo } from 'react'; 2 | import { createTheme } from '@mui/material/styles'; 3 | 4 | 5 | // dark / light mode 6 | export const tokens = (mode) => ({ 7 | ...(mode === 'dark' 8 | ? { 9 | grey: { 10 | 50: '#2E2D3D', 11 | 100: '#e0e0e0', 12 | 200: '#c2c2c2', 13 | 300: '#a3a3a3', 14 | 400: '#858585', 15 | 500: '#666666', 16 | 600: '#525252', 17 | 700: '#3d3d3d', 18 | 800: '#292929', 19 | 900: '#141414', 20 | }, 21 | primary: { 22 | 50: '#2E2D3D', 23 | 100: '#d0d1d5', 24 | 200: '#a1a4ab', 25 | 300: '#727681', 26 | 400: '#1F2A40', 27 | 500: '#222130', //dark grey 28 | // 500: "#141b2d", 29 | 600: '#101624', 30 | 700: '#0c101b', 31 | 800: '#080b12', 32 | 900: '#040509', 33 | }, 34 | greenAccent: { 35 | 100: '#dbf5ee', 36 | 200: '#b7ebde', 37 | 300: '#94e2cd', 38 | 400: '#70d8bd', 39 | 500: '#4cceac', 40 | 600: '#3da58a', 41 | 700: '#2e7c67', 42 | 800: '#1e5245', 43 | 900: '#0f2922', 44 | }, 45 | redAccent: { 46 | 100: '#f8dcdb', 47 | 200: '#f1b9b7', 48 | 300: '#e99592', 49 | 400: '#e2726e', 50 | 500: '#db4f4a', 51 | 600: '#af3f3b', 52 | 700: '#832f2c', 53 | 800: '#58201e', 54 | 900: '#2c100f', 55 | }, 56 | blueAccent: { 57 | 50: '#47B6FF', 58 | 100: '#e1e2fe', 59 | 200: '#c3c6fd', 60 | 300: '#a4a9fc', 61 | 400: '#868dfb', 62 | 500: '#6870fa', 63 | 600: '#535ac8', 64 | 700: '#3e4396', 65 | 800: '#2a2d64', 66 | 900: '#151632', 67 | }, 68 | } 69 | : { 70 | grey: { 71 | 50: '#2E2D3D', 72 | 100: '#141414', 73 | 200: '#292929', 74 | 300: '#3d3d3d', 75 | 400: '#525252', 76 | 500: '#666666', 77 | 600: '#858585', 78 | 700: '#a3a3a3', 79 | 800: '#c2c2c2', 80 | 900: '#e0e0e0', 81 | }, 82 | primary: { 83 | 50: '#2E2D3D', 84 | 100: '#040509', 85 | 200: '#080b12', 86 | 300: '#0c101b', 87 | 400: '#f2f0f0', 88 | 500: '#222130', 89 | // manually changed 90 | // 500: "#141b2d", 91 | 600: '#1F2A40', 92 | 700: '#727681', 93 | 800: '#a1a4ab', 94 | 900: '#d0d1d5', 95 | }, 96 | greenAccent: { 97 | 100: '#0f2922', 98 | 200: '#1e5245', 99 | 300: '#2e7c67', 100 | 400: '#3da58a', 101 | 500: '#4cceac', //green 102 | 600: '#70d8bd', 103 | 700: '#94e2cd', 104 | 800: '#b7ebde', 105 | 900: '#dbf5ee', 106 | }, 107 | redAccent: { 108 | 100: '#2c100f', 109 | 200: '#58201e', 110 | 300: '#832f2c', 111 | 400: '#af3f3b', 112 | 500: '#db4f4a', 113 | 600: '#e2726e', 114 | 700: '#e99592', 115 | 800: '#f1b9b7', 116 | 900: '#f8dcdb', 117 | }, 118 | blueAccent: { 119 | 50: '#47B6FF', 120 | 100: '#151632', 121 | 200: '#2a2d64', 122 | 300: '#3e4396', 123 | 400: '#535ac8', 124 | 500: '#6870fa', //blueee (purple) 125 | 600: '#868dfb', 126 | 700: '#a4a9fc', 127 | 800: '#c3c6fd', 128 | 900: '#e1e2fe', 129 | }, 130 | }), 131 | }); 132 | 133 | // mui theme settings 134 | export const themeSettings = (mode) => { 135 | const colors = tokens(mode); 136 | return { 137 | palette: { 138 | mode: mode, 139 | ...(mode === "dark" 140 | ? { 141 | ////// DARK MODE ////// 142 | primary: { 143 | main: colors.blueAccent[50], 144 | }, 145 | secondary: { 146 | main: colors.greenAccent[500], 147 | }, 148 | info: { 149 | main: colors.blueAccent[500], 150 | }, 151 | thurdary: { 152 | main: "#6870fa", 153 | }, 154 | inputs: { 155 | main: "#2E2D3D", 156 | }, 157 | 158 | neutral: { 159 | dark: colors.grey[50], 160 | main: colors.grey[500], 161 | light: colors.grey[100], 162 | }, 163 | background: { 164 | default: colors.primary[500], 165 | }, 166 | } 167 | : { 168 | ////// LIGHT MODE ////// 169 | primary: { 170 | main: colors.primary[100], 171 | }, 172 | secondary: { 173 | main: colors.greenAccent[500], 174 | }, 175 | neutral: { 176 | dark: colors.grey[50], 177 | main: colors.grey[500], 178 | light: colors.grey[100], 179 | }, 180 | background: { 181 | default: "#fcfcfc", 182 | }, 183 | }), 184 | }, 185 | typography: { 186 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 187 | fontSize: 12, 188 | h1: { 189 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 190 | fontSize: 40, 191 | }, 192 | h2: { 193 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 194 | fontSize: 32, 195 | }, 196 | h3: { 197 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 198 | fontSize: 24, 199 | }, 200 | h4: { 201 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 202 | fontSize: 20, 203 | }, 204 | h5: { 205 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 206 | fontSize: 16, 207 | }, 208 | h6: { 209 | fontFamily: ["Source Sans Pro", "sans-serif"].join(","), 210 | fontSize: 14, 211 | }, 212 | }, 213 | }; 214 | }; 215 | 216 | // context for color mode 217 | export const ColorModeContext = createContext({ 218 | toggleColorMode: () => {}, 219 | }); 220 | 221 | export const useMode = () => { 222 | const [mode, setMode] = useState('dark'); 223 | 224 | const colorMode = useMemo( 225 | () => ({ 226 | toggleColorMode: () => 227 | setMode((prev) => (prev === 'light' ? 'dark' : 'light')), 228 | }), 229 | [] 230 | ); 231 | 232 | const theme = useMemo(() => createTheme(themeSettings(mode)), [mode]); 233 | return [theme, colorMode]; 234 | }; 235 | 236 | 237 | export default tokens; 238 | ///////////OLD THEME////////////// 239 | 240 | // export const headerTheme = createTheme({ 241 | // typography: { 242 | // fontFamily: "Roboto", 243 | // }, 244 | // palette: { 245 | // primary: { 246 | // main: "#6a994e", 247 | // light: "#42a5f5", 248 | // dark: "#1565c0", 249 | // }, 250 | // secondary: { 251 | // main: "#9c6644", 252 | // light: "#42a5f5", 253 | // dark: "#1565c0", 254 | // }, 255 | // }, 256 | // }); 257 | 258 | // //fix this 259 | // export const bodyTheme = createTheme({ 260 | // palette: { 261 | // mode: 'dark', 262 | // primary: { 263 | // main: "#6a994e", 264 | // light: "#42a5f5", 265 | // dark: "#1565c0", 266 | // }, 267 | // secondary: { 268 | // main: "#9c6644", 269 | // light: "#42a5f5", 270 | // dark: "#1565c0", 271 | // }, 272 | // background: { 273 | // paper: "#424242", 274 | // } 275 | // } 276 | // }); 277 | 278 | // //fix this 279 | // export const themeSidebar = createTheme({ 280 | // palette: { 281 | // mode: 'light', 282 | // primary: { 283 | // main: "#6a994e", 284 | // light: "#42a5f5", 285 | // dark: "#1565c0", 286 | // }, 287 | // secondary: { 288 | // main: "#9c6644", 289 | // light: "#42a5f5", 290 | // dark: "#1565c0", 291 | // }, 292 | // background: { 293 | // paper: "#424242", 294 | // } 295 | // } 296 | // }); 297 | 298 | // ///// -------------------------------------------------------------------------------- /client/components/stylesheets/visualizer.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | width: 100%; 5 | height: 100%; 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | font-family: sans-serif; 10 | } 11 | 12 | 13 | 14 | 15 | /* .diagram-container { 16 | width: 100%; 17 | height: 100%; 18 | background: rgb(245, 245, 245); 19 | } 20 | 21 | .custom-node { 22 | min-width: 150px; 23 | background-color: white; 24 | display: flex; 25 | flex-direction: column; 26 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); 27 | } 28 | 29 | .custom-node-header { 30 | flex-grow: 1; 31 | padding: 8px 24px; 32 | margin-bottom: 8px; 33 | } 34 | .custom-node-subheader { 35 | flex-grow: 1; 36 | padding: 8px; 37 | background: #eee; 38 | } 39 | .custom-node-content { 40 | flex-grow: 1; 41 | padding: 0 8px 8px 8px; 42 | } */ 43 | -------------------------------------------------------------------------------- /client/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const ADD_ALERTS = 'ADD_ALERT'; 2 | export const DELETE_ALERT = 'DELETE_ALERT'; 3 | export const UPDATE_ALERT = 'UPDATE_ALERT'; 4 | export const LOG_IN = 'LOG_IN'; 5 | export const LOG_OUT= 'LOG_OUT'; 6 | export const SAVE_DB = 'SAVE_DB'; 7 | export const SELECT_TABLE = 'SELECT_TABLE'; 8 | export const SELECT_DEPTH = 'SELECT_DEPTH'; 9 | export const SELECT_PAGE = 'SELECT_PAGE'; 10 | export const ADD_MONITORS = 'ADD_MONITORS'; 11 | export const UPDATE_MONITOR = 'UPDATE_MONITOR'; 12 | export const ADD_TABLES_WEIGHTS = 'ADD_TABLE_WEIGHTS'; 13 | export const DISPLAY_ALERTS = 'DISPLAY_ALERTS'; 14 | export const DISPLAY_MONITORS = 'DISPLAY_MONITORS'; 15 | export const UPDATE_DASH_DISPLAY_TIME_RANGE = 'UPDATE_DASH_DISPLAY_TIME_RANGE'; -------------------------------------------------------------------------------- /client/containers/AlertContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import AlertBox from '../components/AlertBox'; 4 | import { 5 | Box, 6 | } from "@mui/material"; 7 | 8 | const AlertContainer = () => { 9 | const alerts = useSelector((state) => state.alert.alerts); 10 | const displayAlertsArrFromState = useSelector((state) => state.alert.displayAlerts); 11 | 12 | const [displayAlertsArr, setDisplayAlertsArr] = useState(displayAlertsArrFromState); 13 | const [anomalies, setAnomalies] = useState([]); 14 | 15 | useEffect(() => { 16 | const sortedAlerts = displayAlertsArrFromState.sort((a, b) => new Date(b.detected_at) - new Date(a.detected_at)); 17 | setDisplayAlertsArr(sortedAlerts); 18 | const anomaliesAsComponents = sortedAlerts.map((alertObj, i) => ); 19 | setAnomalies(anomaliesAsComponents); 20 | }, [alerts, displayAlertsArrFromState]); 21 | 22 | // allow user to filter alerts by 23 | // table 24 | // column 25 | // monitorType 26 | // resolved / not 27 | // dismissed / not 28 | // date 29 | 30 | // pull from state - alert.filterBy 31 | // filter anomalies thusly 32 | 33 | return ( 34 | 37 |
{anomalies}
38 |
39 | ); 40 | } 41 | 42 | export default AlertContainer; -------------------------------------------------------------------------------- /client/containers/AppContainer.jsx: -------------------------------------------------------------------------------- 1 | import io from 'socket.io-client' 2 | import React, { useEffect, useState } from 'react'; 3 | import { Box } from '@mui/material'; 4 | import { LocalizationProvider } from '@mui/x-date-pickers'; 5 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' 6 | 7 | 8 | //TEMPORARY IMPORTS 9 | import { useDispatch, useSelector } from 'react-redux'; 10 | import { saveDBActionCreator, addAlertsActionCreator, addMonitorsActionCreator } from '../actions/actions'; 11 | import AlertBox from '../components/AlertBox'; 12 | //END TEMPORARY IMPORTS 13 | 14 | import Header from '../components/Header'; 15 | import SideBar from '../components/SideBar'; 16 | import ErdVisualizerContainer from '../containers/ErdVisualizerContainer'; 17 | import MonitorContainer from '../containers/MonitorContainer'; 18 | import CustomRangesMonitor from '../components/monitors/RangeMonitor'; 19 | import PageContainer from './PageContainer'; 20 | import SubheaderContainer from './SubheaderContainer'; 21 | // import { response } from "express"; 22 | import MainContainer from './MainContainer'; 23 | 24 | //////////////////////hay added for light/dark mode///////////////////// 25 | // import { ThemeProvider } from '@emotion/react'; 26 | import { ColorModeContext, useMode } from '../components/stylesheets/Themes'; 27 | import { CssBaseline, ThemeProvider } from '@mui/material'; 28 | import LandingContainer from './LandingContainer'; 29 | 30 | //for pull out drawer: 31 | // import Topbar from "./scenes/global/Topbar"; 32 | 33 | const AppContainer = () => { 34 | //light/dark mode 35 | const [theme, colorMode] = useMode(); 36 | //for pullout drawer: 37 | // const [isSideBar, setIsSideBar] = useState(true); 38 | /* */ 39 | 40 | const dispatch = useDispatch(); 41 | 42 | const user_uri = useSelector((state) => state.user.uri); 43 | 44 | useEffect(() => { 45 | const fetchDB = async () => { 46 | try { 47 | const requestOptions = { 48 | method: 'POST', 49 | headers: { 'Content-Type': 'application/json' }, 50 | body: JSON.stringify({user_uri: user_uri}) 51 | }; 52 | const response = await fetch('/eboshi', requestOptions); 53 | const data = await response.json(); 54 | console.log('dbArray in fetchDB in appContainer: ', data.dbArray); 55 | if (!response.ok) throw new Error(data.error || 'Error from server'); 56 | dispatch(saveDBActionCreator(data.dbArray)); 57 | } catch (err) { 58 | console.log('AppContainer Mounted', err); 59 | } 60 | }; 61 | fetchDB(); 62 | }, []); 63 | 64 | const user_id = useSelector((state) => state.user.user_id); 65 | 66 | useEffect(() => { 67 | fetchAllMonitors(); 68 | getAllAlerts(); 69 | if (user_id) { 70 | const socket = io('http://localhost:3000'); 71 | socket.on('connect', () => { 72 | socket.emit('register', { user_id: user_id }); 73 | }); 74 | socket.on('alert', (alerts) => { 75 | console.log('(人´∀`).☆.。.: SOCKET.IO IN APPCONTAINER RECIEVED ALERTS :.。.☆.(´∀`人)', alerts); 76 | dispatch(addAlertsActionCreator(alerts)); 77 | }); 78 | 79 | return () => { 80 | socket.disconnect(); 81 | } 82 | } 83 | }, [user_id]) 84 | 85 | 86 | const fetchAllMonitors = async () => { 87 | console.log('user_id in fetchAllMonitors in MonitorContainer', user_id); 88 | try { 89 | const response = await fetch('/getMonitors', { 90 | method: 'POST', 91 | headers: { 92 | 'Content-Type': 'application/json' 93 | }, 94 | body: JSON.stringify({user_id: user_id}) 95 | }); 96 | if (!response.ok) { 97 | throw new Error(`HTTP error! status: ${response.status}`); 98 | } 99 | const data = await response.json(); 100 | console.log('data in fetchAllMonitors in AppContainer', data); 101 | dispatch(addMonitorsActionCreator(data)); 102 | } catch (error) { 103 | console.log('fetch error:', error); 104 | } 105 | }; 106 | 107 | // useEffect(() => { 108 | // fetchAllMonitors(); 109 | // }, []); 110 | 111 | const getAllAlerts = async () => { 112 | try { 113 | const response = await fetch('/alerts', { 114 | method: 'POST', 115 | headers: { 116 | 'Content-Type': 'application/json' 117 | }, 118 | body: JSON.stringify({user_id: user_id}) 119 | }); 120 | if(!response.ok) { 121 | throw new Error(`HTTP error! status: ${response.status}`); 122 | } 123 | const data = await response.json(); 124 | console.log('data in getallalerts in alertcontainer: ', data); 125 | dispatch(addAlertsActionCreator(data)); 126 | } catch (error) { 127 | console.log('fetch error:', error); 128 | } 129 | }; 130 | 131 | // useEffect(() => { 132 | // getAllAlerts(); 133 | // }, []); 134 | 135 | return ( 136 | 137 | 138 | 139 |
140 | 141 |
142 | 143 | 144 | 151 | 152 | 160 | 161 | 162 | 163 | 164 |
165 |
166 |
167 |
168 | ); 169 | }; 170 | 171 | export default AppContainer; -------------------------------------------------------------------------------- /client/containers/DashboardContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DashTableOfTables from '../components/DashTableOfTables'; 3 | import DashAlertLineChart from '../components/DashAlertLineChart'; 4 | import DashAlertBarChart from '../components/DashAlertBarChart'; 5 | import Box from '@mui/material/Box' 6 | 7 | const DashboardContainer = () => { 8 | return ( 9 | 20 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default DashboardContainer; 39 | -------------------------------------------------------------------------------- /client/containers/ErdVisualizerContainer.jsx: -------------------------------------------------------------------------------- 1 | // import * as React from "react"; 2 | // import { Box, Container } from "@mui/material"; 3 | // // import ErdVisualizer from "../components/ErdVisualizer"; 4 | // import Focus from "../components/Focus"; 5 | // import FocusBar from "../components/FocusBar"; 6 | 7 | // const ErdVisualizerContainer = () => { 8 | // return ( 9 | //
10 | // 11 | // 25 | // 26 | 27 | // {/* */} 28 | // 29 | // 30 | // 31 | //
32 | // ); 33 | // }; 34 | 35 | // export default ErdVisualizerContainer; 36 | 37 | import * as React from "react"; 38 | import { Box, Container } from "@mui/material"; 39 | // import ErdVisualizer from "../components/ErdVisualizer"; 40 | import Focus from "../components/Focus"; 41 | import FocusBar from "../components/FocusBar"; 42 | 43 | const ErdVisualizerContainer = () => { 44 | return ( 45 |
46 | 47 | 48 | 49 |
50 | ); 51 | }; 52 | 53 | export default ErdVisualizerContainer; -------------------------------------------------------------------------------- /client/containers/LandingContainer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Box, 4 | Toolbar, 5 | Button, 6 | ThemeProvider, 7 | useTheme, 8 | Typography, 9 | } from '@mui/material'; 10 | import { useSelector } from 'react-redux'; 11 | import { ColorModeContext, tokens } from '../components/stylesheets/Themes.jsx'; 12 | import { useContext, useState } from 'react'; 13 | import LoginContainer from '../components/Login.jsx'; 14 | 15 | const LandingContainer = () => { 16 | const theme = useTheme(); 17 | // const colors = tokens(theme.palette.mode); 18 | // const colorMode = useContext(ColorModeContext); 19 | 20 | const [showLogin, setShowLogin] = useState(false); 21 | 22 | return ( 23 |
29 | 30 | 31 | 41 | 42 |
50 | {showLogin && setShowLogin(false)} />} 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default LandingContainer; 57 | -------------------------------------------------------------------------------- /client/containers/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Container } from '@mui/material'; 3 | import Focus from '../components/Focus'; 4 | import FocusBar from '../components/FocusBar'; 5 | import Header from '../components/Header'; 6 | import SideBar from '../components/SideBar'; 7 | import DashboardContainer from './DashboardContainer'; 8 | // import { theme1, theme2 } from "../public/styles/theme"; 9 | 10 | const MainContainer = () => { 11 | return ( 12 |
13 | 14 | {/* */} 15 | 16 | {/* */} 17 | {/*
*/} 18 | 19 | 30 | 40 | {/* */} 41 | 42 | {/* */} 43 | 44 | {/*
*/} 45 | {/* */} 46 | 47 | ); 48 | }; 49 | 50 | export default MainContainer; 51 | -------------------------------------------------------------------------------- /client/containers/PageContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ErdVisualizerContainer from './ErdVisualizerContainer'; 3 | import MonitorContainer from './MonitorContainer' 4 | import AlertContainer from './AlertContainer'; 5 | import ReportContainer from './ReportContainer' 6 | import QueryContainer from './QueryContainer'; 7 | import { useSelector } from 'react-redux'; 8 | import { Box } from '@mui/system'; 9 | import LoginContainer from '../components/Login'; 10 | import MainContainer from './MainContainer'; 11 | 12 | const PageContainer = () => { 13 | const page = useSelector((state) => state.app.page); 14 | 15 | return ( 16 | 26 | {page === 'Dashboard' && } 27 | {page === 'ERD' && } 28 | {page === 'Monitors' && } 29 | {page === 'Alerts' && } 30 | {page === 'Query' && } 31 | {page === 'Account' && <>} 32 | {page === 'Settings' && <>} 33 | {page === 'Help' && <>} 34 | 35 | ); 36 | }; 37 | 38 | export default PageContainer; 39 | -------------------------------------------------------------------------------- /client/containers/QueryContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const QueryContainer = () => { 4 | return ( 5 |
6 |

Future plans: AI/ML query helper that suggests effective queries.

7 |
8 | ) 9 | } 10 | 11 | export default QueryContainer; -------------------------------------------------------------------------------- /client/containers/ReportContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AlertContainer from './AlertContainer'; 3 | 4 | const ReportContainer = () => { 5 | 6 | return( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default ReportContainer; -------------------------------------------------------------------------------- /client/containers/SubheaderContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { Box } from '@mui/system'; 4 | import ErdVisualizerContainer from './ErdVisualizerContainer'; 5 | import MonitorContainer from './MonitorContainer'; 6 | import ReportContainer from './ReportContainer'; 7 | import QueryContainer from './QueryContainer'; 8 | import FocusBar from '../components/FocusBar'; 9 | import MonitorHeader from '../components/MonitorHeader'; 10 | import DashboardHeader from '../components/DashboardHeader'; 11 | import AlertsHeader from '../components/AlertsHeader'; 12 | import QueryHeader from '../components/QueryHeader'; 13 | 14 | 15 | const SubheaderContainer = () => { 16 | const page = useSelector((state) => state.app.page) 17 | 18 | return ( 19 | // 20 | 21 | {page === 'Dashboard' && } 22 | {page === 'ERD' && } 23 | {page === 'Monitors' && } 24 | {page === 'Alerts' && } 25 | {page === 'Query' && } 26 | {page === 'Account' && <>} 27 | {page === 'Settings' && <>} 28 | {page === 'Help' && <>} 29 | 30 | ); 31 | } 32 | 33 | export default SubheaderContainer; -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | moniQL 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | import ReactDom from 'react-dom/client'; 4 | import { Provider } from 'react-redux'; 5 | import store from './store'; 6 | 7 | const root = ReactDom.createRoot(document.getElementById('root')); 8 | 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | 15 | -------------------------------------------------------------------------------- /client/reducers/alertReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | alerts: [], 5 | displayAlerts: [], 6 | }; 7 | 8 | 9 | const alertReducer = (state = initialState, action) => { 10 | let updatedAlerts; 11 | let displayAlerts; 12 | 13 | switch(action.type) { 14 | case types.ADD_ALERTS: 15 | 16 | updatedAlerts = state.alerts.slice(); 17 | updatedAlerts.push(...action.payload); 18 | updatedAlerts = updatedAlerts.sort((a, b) => new Date(b.detected_at) - new Date(a.detected_at)); 19 | 20 | displayAlerts = updatedAlerts.filter(alert => alert.display === true); 21 | 22 | return { 23 | ...state, 24 | alerts: updatedAlerts, 25 | displayAlerts: displayAlerts 26 | } 27 | 28 | case types.DELETE_ALERT: 29 | 30 | updatedAlerts = state.alerts.slice() 31 | updatedAlerts = updatedAlerts.filter(alert => alert.alert_id !== action.payload.alert_id); 32 | 33 | return { 34 | ...state, 35 | alerts: updatedAlerts 36 | } 37 | 38 | case types.UPDATE_ALERT: 39 | 40 | updatedAlerts = state.alerts.slice(); 41 | updatedAlerts = updatedAlerts.map((alert) => { 42 | if (alert.alert_id === action.payload.alert_id) { 43 | return action.payload; 44 | } 45 | return alert; 46 | }); 47 | updatedAlerts = updatedAlerts.sort((a, b) => new Date(b.detected_at) - new Date(a.detected_at)); 48 | 49 | return { 50 | ...state, 51 | alerts: updatedAlerts 52 | } 53 | 54 | case types.DISPLAY_ALERTS: 55 | updatedAlerts = action.payload.sort((a, b) => new Date(b.detected_at) - new Date(a.detected_at)); 56 | 57 | return { 58 | ...state, 59 | displayAlerts: updatedAlerts 60 | } 61 | 62 | default: 63 | return state; 64 | } 65 | 66 | }; 67 | 68 | export default alertReducer; 69 | -------------------------------------------------------------------------------- /client/reducers/appReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | page: 'Dashboard' 5 | // page: 'Alerts' 6 | }; 7 | 8 | const appReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case types.SELECT_PAGE: 11 | return { 12 | ...state, 13 | page: action.payload 14 | } 15 | 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export default appReducer; -------------------------------------------------------------------------------- /client/reducers/diagramReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | data: [], 5 | focusTable: null, 6 | depth: 6, 7 | tablesWeightsObj: {}, 8 | dashDisplayAlertsTimeRange: [new Date() - 1000 * 60 * 60 * 24 * 7, Date.now()], 9 | }; 10 | 11 | const diagramReducer = (state = initialState, action) => { 12 | switch (action.type) { 13 | 14 | case types.SAVE_DB: 15 | return { 16 | ...state, 17 | data: action.payload, 18 | } 19 | 20 | case types.SELECT_TABLE: 21 | return{ 22 | ...state, 23 | focusTable: action.payload, 24 | } 25 | 26 | case types.SELECT_DEPTH: 27 | return{ 28 | ...state, 29 | depth: action.payload, 30 | } 31 | 32 | case types.ADD_TABLES_WEIGHTS: 33 | return{ 34 | ...state, 35 | tablesWeightsObj: action.payload, 36 | } 37 | 38 | case types.UPDATE_DASH_DISPLAY_TIME_RANGE: 39 | return { 40 | ...state, 41 | dashDisplayAlertsTimeRange: action.payload, 42 | } 43 | 44 | default: 45 | return state; 46 | } 47 | }; 48 | 49 | export default diagramReducer; 50 | -------------------------------------------------------------------------------- /client/reducers/monitorReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | const initialState = { 4 | activeMonitors: [], 5 | displayMonitors: [] 6 | }; 7 | 8 | const monitorReducer = (state = initialState, action) => { 9 | let updatedMonitors; 10 | let updatedDisplayMonitors; 11 | 12 | switch(action.type) { 13 | case types.ADD_MONITORS: 14 | console.log('payload in monitorReducer: ', action.payload) 15 | 16 | return { 17 | ...state, 18 | activeMonitors: action.payload, 19 | displayMonitors: action.payload 20 | } 21 | 22 | case types.DISPLAY_MONITORS: 23 | 24 | return { 25 | ...state, 26 | displayMonitors: action.payload 27 | } 28 | 29 | case types.UPDATE_MONITOR: 30 | 31 | updatedMonitors = state.activeMonitors.slice(); 32 | updatedMonitors = updatedMonitors.map((monitor) => { 33 | if (monitor.monitor_id === action.payload.monitor_id) { 34 | return action.payload; 35 | } 36 | return monitor; 37 | }); 38 | 39 | updatedDisplayMonitors = state.displayMonitors.slice(); 40 | updatedDisplayMonitors = updatedDisplayMonitors.map((monitor) => { 41 | if (monitor.monitor_id === action.payload.monitor_id) { 42 | return action.payload; 43 | } 44 | return monitor; 45 | }); 46 | 47 | return { 48 | ...state, 49 | activeMonitors: updatedMonitors, 50 | displayMonitors: updatedDisplayMonitors 51 | } 52 | 53 | 54 | default: 55 | return state; 56 | } 57 | }; 58 | 59 | export default monitorReducer; -------------------------------------------------------------------------------- /client/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | //Here are the different slices of our state, each managed by a different reducer file. 4 | //The way Redux works, when we fire off a dispatch, it will be sent through each reducer file. 5 | import alertReducer from "./alertReducer"; 6 | import diagramReducer from "./diagramReducer"; 7 | import userReducer from "./userReducer"; 8 | import appReducer from "./appReducer"; 9 | import monitorReducer from "./monitorReducer"; 10 | 11 | //combineReducers method combines our reducers so we can import them all as one to the store.js file! 12 | //if we ended up with more reducer files, they would go here. 13 | //this helps keep things clean and scalable! ⋆。°✩ヽ( ⌒o⌒)人(⌒-⌒ )ノ✩°。⋆ 14 | const rootReducer = combineReducers({ 15 | alert: alertReducer, 16 | diagram: diagramReducer, 17 | user: userReducer, 18 | app: appReducer, 19 | monitor: monitorReducer, 20 | }); 21 | 22 | export default rootReducer; 23 | 24 | -------------------------------------------------------------------------------- /client/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../constants/actionTypes'; 2 | 3 | // for dev mode: 4 | const initialState = { 5 | user_id: null, 6 | username: '', 7 | uri: '', 8 | isLoggedIn: false, 9 | }; 10 | 11 | // for prod mode: 12 | // const initialState = { 13 | // user_id: null, 14 | // username: null, 15 | // uri: null, 16 | // isLoggedIn: false, 17 | // }; 18 | 19 | const userReducer = (state = initialState, action) => { 20 | switch (action.type) { 21 | case types.LOG_IN: 22 | return { 23 | ...state, 24 | user_id: action.payload.user_id, 25 | username: action.payload.username, 26 | uri: action.payload.uri, 27 | isLoggedIn: true, 28 | }; 29 | case types.LOG_OUT: 30 | return { 31 | ...state, 32 | isLoggedIn: false, 33 | }; 34 | default: 35 | return state; 36 | } 37 | }; 38 | 39 | export default userReducer; 40 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import {configureStore} from '@reduxjs/toolkit'; 2 | 3 | import rootReducer from './reducers/rootReducer'; 4 | 5 | //configure store is the method from toolkit that we use to combine our slices. 6 | //the devTools option I grabbed from a CodeSmith unit. It ensures that we aren't wasting compute by running dev tools in production mode (npm start) 7 | const store = configureStore({ 8 | reducer: rootReducer, 9 | devTools: process.env.NODE_ENV !== 'production', 10 | }) 11 | // (⌐⊙_⊙) -- guess where we are going to import this... -- (⊙_⊙¬) \\ 12 | export default store; -------------------------------------------------------------------------------- /init.sql: -------------------------------------------------------------------------------- 1 | -- Sequence for users 2 | CREATE SEQUENCE public.users_user_id_seq 3 | AS integer 4 | START WITH 1 5 | INCREMENT BY 1 6 | NO MINVALUE 7 | NO MAXVALUE 8 | CACHE 1; 9 | 10 | -- Sequence for uris 11 | CREATE SEQUENCE public.uris_uri_id_seq 12 | AS integer 13 | START WITH 1 14 | INCREMENT BY 1 15 | NO MINVALUE 16 | NO MAXVALUE 17 | CACHE 1; 18 | 19 | -- Sequence for monitors 20 | CREATE SEQUENCE public.monitors_monitor_id_seq 21 | AS integer 22 | START WITH 1 23 | INCREMENT BY 1 24 | NO MINVALUE 25 | NO MAXVALUE 26 | CACHE 1; 27 | 28 | -- Sequence for stats 29 | CREATE SEQUENCE public.stats_stats_id_seq 30 | AS integer 31 | START WITH 1 32 | INCREMENT BY 1 33 | NO MINVALUE 34 | NO MAXVALUE 35 | CACHE 1; 36 | 37 | -- Table: uris 38 | CREATE TABLE public.uris ( 39 | uri_id integer NOT NULL DEFAULT nextval('public.uris_uri_id_seq'::regclass), 40 | uri text NOT NULL, 41 | PRIMARY KEY (uri_id) 42 | ); 43 | 44 | -- Table: users 45 | CREATE TABLE public.users ( 46 | user_id integer NOT NULL DEFAULT nextval('public.users_user_id_seq'::regclass), 47 | username character varying(32) NOT NULL, 48 | password character varying(255) NOT NULL, 49 | uri_id integer NOT NULL, 50 | PRIMARY KEY (user_id), 51 | UNIQUE (username), 52 | FOREIGN KEY (uri_id) REFERENCES public.uris(uri_id) 53 | ); 54 | 55 | -- Table: monitors 56 | CREATE TABLE public.monitors ( 57 | monitor_id integer NOT NULL DEFAULT nextval('public.monitors_monitor_id_seq'::regclass), 58 | type character varying(16) NOT NULL, 59 | user_id integer NOT NULL, 60 | parameters json, 61 | PRIMARY KEY (monitor_id), 62 | FOREIGN KEY (user_id) REFERENCES public.users(user_id) 63 | ); 64 | 65 | -- Table: alerts 66 | CREATE TABLE public.alerts ( 67 | alert_id uuid NOT NULL, 68 | user_id integer, 69 | alert_obj json NOT NULL, 70 | PRIMARY KEY (alert_id), 71 | FOREIGN KEY (user_id) REFERENCES public.users(user_id) 72 | ); 73 | 74 | -- Table: stats 75 | CREATE TABLE public.stats ( 76 | stats_id integer NOT NULL DEFAULT nextval('public.stats_stats_id_seq'::regclass), 77 | monitor_id integer, 78 | table_name character varying(255), 79 | num_rows integer, 80 | stats_obj json, 81 | starting timestamp with time zone, 82 | ending timestamp with time zone, 83 | PRIMARY KEY (stats_id), 84 | FOREIGN KEY (monitor_id) REFERENCES public.monitors(monitor_id) 85 | ); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hide-n-go-sql", 3 | "version": "1.0.0", 4 | "description": "SQL and you shall find", 5 | "main": "/client/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "NODE_ENV=production nodemon server/server.js", 9 | "dev": "concurrently \"cross-env NODE_ENV=development webpack-dev-server --open --hot --color\" \"cross-env NODE_ENV=development nodemon ./server/server.js\"", 10 | "build": "webpack --mode development" 11 | }, 12 | "nodemonConfig": { 13 | "ignore": [ 14 | "build", 15 | "client" 16 | ] 17 | }, 18 | "author": "! ~*h4ck3r5*~ !", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "@babel/core": "^7.23.7", 22 | "@babel/preset-env": "^7.23.8", 23 | "@babel/preset-react": "^7.23.3", 24 | "babel-loader": "^9.1.3", 25 | "concurrently": "^8.2.2", 26 | "cross-env": "^7.0.3", 27 | "css-loader": "^6.9.0", 28 | "eslint": "^8.56.0", 29 | "eslint-plugin-react": "^7.33.2", 30 | "eslint-plugin-react-hooks": "^4.6.0", 31 | "html-webpack-plugin": "^5.6.0", 32 | "sass": "^1.69.7", 33 | "sass-loader": "^13.3.3", 34 | "style-loader": "^3.3.4", 35 | "url-loader": "^4.1.1", 36 | "webpack": "^5.89.0", 37 | "webpack-cli": "^5.1.4", 38 | "webpack-dev-server": "^4.15.1" 39 | }, 40 | "dependencies": { 41 | "@emotion/react": "^11.11.3", 42 | "@emotion/styled": "^11.11.0", 43 | "@mui/icons-material": "^5.15.5", 44 | "@mui/material": "^5.15.5", 45 | "@mui/x-charts": "^6.19.3", 46 | "@mui/x-data-grid": "^6.19.3", 47 | "@mui/x-date-pickers": "^6.19.3", 48 | "@reduxjs/toolkit": "^2.0.1", 49 | "bcrypt": "^5.1.1", 50 | "cors": "^2.8.5", 51 | "dayjs": "^1.11.10", 52 | "dotenv": "^16.3.1", 53 | "express": "^4.18.2", 54 | "mui": "^0.0.1", 55 | "node-cron": "^3.0.3", 56 | "node-fetch": "^3.3.2", 57 | "nodemon": "^3.0.2", 58 | "pg": "^8.11.3", 59 | "react": "^18.2.0", 60 | "react-dom": "^18.2.0", 61 | "react-flow-renderer": "^10.3.17", 62 | "react-pro-sidebar": "^1.1.0-alpha.2", 63 | "react-redux": "^9.1.0", 64 | "react-router-dom": "^6.22.0", 65 | "reactflow": "^11.10.2", 66 | "redux": "^5.0.1", 67 | "socket.io": "^4.7.4", 68 | "socket.io-client": "^4.7.4", 69 | "styled-components": "latest", 70 | "uuid": "^9.0.1" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/controllers/dbController.js: -------------------------------------------------------------------------------- 1 | const { userPool, connectToPool } = require('../models/db'); 2 | const { Pool } = require('pg'); 3 | const fs = require('fs').promises; 4 | require('dotenv').config(); 5 | // const userUri = process.env.USER_URI || undefined; 6 | 7 | 8 | // const db = userPool; 9 | let db; 10 | 11 | const dbController = {}; 12 | // dbController.connect = async (req, res, next) => { 13 | // const dbConnect = res.locals.uri; 14 | // try { 15 | // // console.log('userURI in dbcontroller try block: ', userUri) 16 | // // if (!userUri) { 17 | // // // fs.appendFileSync('.env' , '\n' + 'USER_URI=' + dbConnect); 18 | // // // fs.appendFile 19 | 20 | // // console.log('Successfully wrote URI to file'); 21 | // // } 22 | // // read the env, parse it into js object, reassign the user uri property, rewrite the .env file, plugging in new obj 23 | // // process.env.USER_URI = dbConnect; 24 | // // console.log(' THIS IS OUR DBCONNECT IN DBCONTROLLER: ', process.env.USER_URI) 25 | // return next() 26 | // } catch (err) { 27 | // console.error('Error writing file:', err); 28 | // return next(err); 29 | // } 30 | // } 31 | 32 | dbController.getDB = async (req, res, next) => { 33 | try { 34 | 35 | const { user_uri } = req.body; 36 | db = await connectToPool(user_uri); 37 | 38 | const query = ` 39 | SELECT 40 | table_info.table_name, 41 | table_info.columns, 42 | fk_info.foreign_keys 43 | FROM 44 | (SELECT 45 | c.table_name, 46 | json_agg(json_build_object('name', c.column_name, 'data_type', c.data_type, 'is_primary', (c.column_name = ANY(pk_info.pkey_columns)), 'is_foreign', (c.column_name = ANY(fk_info.fkey_columns))) ORDER BY (c.column_name = ANY(pk_info.pkey_columns)) DESC, (c.column_name = ANY(fk_info.fkey_columns)) DESC, c.column_name) AS columns 47 | FROM 48 | information_schema.columns c 49 | LEFT JOIN 50 | (SELECT 51 | tc.table_name, 52 | array_agg(kcu.column_name) AS pkey_columns 53 | FROM 54 | information_schema.table_constraints tc 55 | JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema 56 | WHERE 57 | tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' 58 | GROUP BY 59 | tc.table_name 60 | ) AS pk_info ON c.table_name = pk_info.table_name 61 | LEFT JOIN 62 | (SELECT 63 | tc.table_name, 64 | array_agg(kcu.column_name) AS fkey_columns 65 | FROM 66 | information_schema.table_constraints tc 67 | JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema 68 | WHERE 69 | tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' 70 | GROUP BY 71 | tc.table_name 72 | ) AS fk_info ON c.table_name = fk_info.table_name 73 | WHERE 74 | c.table_schema = 'public' 75 | GROUP BY 76 | c.table_name 77 | ) AS table_info 78 | LEFT JOIN 79 | (SELECT 80 | tc.table_name, 81 | json_agg(json_build_object('foreign_table', ccu.table_name, 'column', kcu.column_name, 'foreign_column', ccu.column_name)) AS foreign_keys 82 | FROM 83 | information_schema.table_constraints AS tc 84 | JOIN information_schema.key_column_usage AS kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema 85 | JOIN information_schema.constraint_column_usage AS ccu ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema 86 | WHERE 87 | tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' 88 | GROUP BY 89 | tc.table_name 90 | ) AS fk_info 91 | ON 92 | table_info.table_name = fk_info.table_name; 93 | ` 94 | 95 | const results = await db.query(query); 96 | // console.log('*********results in getDB: ', results) 97 | res.locals.dbArray = results.rows 98 | 99 | db.end() 100 | .then(() => console.log('db connection closed in dbController.getDB')) 101 | .catch(err => console.error('Error closing db connection in dbController.getDB:', err)); 102 | return next() 103 | } catch (err) { 104 | return next(err) 105 | } 106 | } 107 | //SELECT * FROM "public"."people" LIMIT 100 108 | 109 | 110 | // const theirPool = new Pool({ 111 | // connectionString: dbConnect 112 | // }) 113 | 114 | module.exports = dbController; 115 | 116 | 117 | 118 | /* 119 | // Assume that process.env.PORT was initially set to 3000 120 | console.log('Initial PORT:', process.env.PORT); // Output: 3000 121 | 122 | // Update PORT during runtime -- works, but db.js has already run by the time it reassigns process.env 123 | process.env.PORT = 4000; 124 | console.log('Updated PORT:', process.env.PORT); // Output: 4000 125 | 126 | */ 127 | 128 | // took this out of the function, couldn't look at it anymore -a 129 | // const query = `SELECT 130 | // table_info.table_name, 131 | // table_info.columns, 132 | // fk_info.foreign_keys 133 | // FROM 134 | // (SELECT 135 | // table_name, 136 | // json_agg(column_name) AS columns 137 | // FROM 138 | // information_schema.columns 139 | // WHERE 140 | // table_schema = 'public' 141 | // GROUP BY 142 | // table_name 143 | // ) AS table_info 144 | // LEFT JOIN 145 | // (SELECT 146 | // tc.table_name, 147 | // json_agg(json_build_object('foreign_table', ccu.table_name, 'column', kcu.column_name, 'foreign_column', ccu.column_name)) AS foreign_keys 148 | // FROM 149 | // information_schema.table_constraints AS tc 150 | // JOIN information_schema.key_column_usage AS kcu 151 | // ON tc.constraint_name = kcu.constraint_name 152 | // AND tc.table_schema = kcu.table_schema 153 | // JOIN information_schema.constraint_column_usage AS ccu 154 | // ON ccu.constraint_name = tc.constraint_name 155 | // AND ccu.table_schema = tc.table_schema 156 | // WHERE 157 | // tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public' 158 | // GROUP BY 159 | // tc.table_name 160 | // ) AS fk_info 161 | // ON 162 | // table_info.table_name = fk_info.table_name;` -------------------------------------------------------------------------------- /server/models/db.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | require('dotenv').config(); 3 | const fs = require('fs'); 4 | const pgUri = process.env.PG_URI; 5 | const userUri = process.env.USER_URI; 6 | // const userUri = fs.readfile('..uri.txt'); 7 | 8 | const connectToPool = async (uri) => { 9 | const newPool = new Pool({ 10 | connectionString: uri 11 | }); 12 | return newPool; 13 | } 14 | 15 | const pool = new Pool({ 16 | connectionString: pgUri 17 | }); 18 | 19 | // uncomment for dev mode 20 | // const userPool = new Pool({ 21 | // connectionString: userUri 22 | // }); 23 | 24 | 25 | module.exports = { 26 | connectToPool, 27 | pool, 28 | // userPool, 29 | query: (text, params, callback) => { 30 | console.log('executed query', text); 31 | return pool.query(text, params, callback); 32 | }, 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /server/routers/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const userController = require('../controllers/userController'); 4 | const dbController = require('../controllers/dbController'); 5 | const monitorController = require('../controllers/monitorController'); 6 | 7 | //uncomment below to revert to working login 8 | // router.post('/login', userController.login, monitorController.connect, (req, res) => {res.status(200).json(res.locals)}); 9 | 10 | router.post('/login', userController.login, monitorController.connect, userController.getMonitors, monitorController.scheduleMonitors, (req, res) => {res.status(200).json(res.locals)}); 11 | 12 | router.post('/register', userController.register, monitorController.connect, userController.getMonitors, monitorController.scheduleMonitors, (req, res) => {res.status(200).json(res.locals)}); 13 | 14 | router.post('/people', dbController.getDB, (req, res) => {res.status(200).json(res.locals)}); 15 | 16 | router.post('/monitor', monitorController.queryAll, (req, res) => {res.status(200).json(res.locals)}); 17 | 18 | router.post('/volume', monitorController.volume, (req, res) => {res.status(200).json(res.locals)}); 19 | 20 | router.post('/fresh', monitorController.fresh, (req, res) => {res.status(200).json(res.locals)}); 21 | 22 | router.post('/range', monitorController.range, userController.addAlerts, (req, res) => {res.status(200).json(res.locals)}); 23 | 24 | router.post('/null', monitorController.null, userController.addAlerts, (req, res) => {res.status(200).json(res.locals)}); 25 | 26 | router.post('/getMonitors', userController.getMonitors, (req, res) => {res.status(200).json(res.locals.monitors)}); 27 | 28 | router.post('/monitors', userController.insertMonitor, monitorController.scheduleMonitors, userController.getMonitors, (req, res) => {res.status(200).json(res.locals.monitors)}); 29 | 30 | router.put('/monitors', userController.updateMonitor, (req, res) => {res.status(200).json(res.locals.monitors)}); 31 | 32 | router.post('/alerts', userController.getAlerts, (req, res) => {res.status(200).json(res.locals.allAlerts)}); 33 | 34 | router.put('/alerts', userController.updateAlert, (req, res) => {res.status(200).json('all good')}); 35 | 36 | router.post('/stats', monitorController.stats, (req, res) => {res.status(200).json(res.locals)}); 37 | 38 | router.post('/custom', monitorController.custom, userController.addAlerts, (req, res) => {res.status(200).json(res.locals)}); 39 | 40 | //this is temp for working on front end 41 | router.post('/eboshi', dbController.getDB, (req, res) => {res.status(200).json(res.locals)}); 42 | module.exports = router; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const http = require('http'); 3 | 4 | const path = require('path'); 5 | const cors = require('cors'); 6 | const apiRouter = require('./routers/api') 7 | const { init } = require('./socket.js') 8 | 9 | const PORT = 3000; 10 | const app = express(); 11 | 12 | //we create this wrapper for our express server using Node's native http module in order to use Socket.io 13 | //This is because 14 | const server = http.createServer(app); 15 | const io = init(server) 16 | 17 | app.use(cors({origin: '*'})); 18 | 19 | app.use(express.json()); 20 | 21 | app.use(express.static(path.resolve(__dirname, '../build'))); 22 | 23 | app.use('/', apiRouter); 24 | 25 | 26 | 27 | 28 | app.use((err, req, res, next) => { 29 | console.log('error in app.use: ', err) 30 | const defaultErr = { 31 | log: 'Express error handler caught unknown middleware error', 32 | status: 500, 33 | message: { err: 'An error occurred' }, 34 | }; 35 | const errorObj = Object.assign({}, defaultErr, err); 36 | console.log(errorObj.log); 37 | return res.status(errorObj.status).json(errorObj.message); 38 | }); 39 | 40 | server.listen(PORT, () => console.log('HEY LISTEN! (on 3000)')); 41 | 42 | -------------------------------------------------------------------------------- /server/socket.js: -------------------------------------------------------------------------------- 1 | const { Server } = require('socket.io'); 2 | let io; 3 | 4 | const init = (httpServer) => { 5 | io = new Server(httpServer, { 6 | cors: { 7 | origin: '*', 8 | methods: ['GET', 'POST'], 9 | }, 10 | }); 11 | 12 | io.on('connection', (socket) => { 13 | 14 | socket.on('register', ({ user_id }) => { 15 | socket.join(user_id.toString()); 16 | console.log(`User with ID: ${user_id} has joined their room.`); 17 | }); 18 | 19 | socket.on('disconnect', () => { 20 | console.log('User disconnected'); 21 | }); 22 | }); 23 | 24 | return io; 25 | }; 26 | 27 | const getIo = () => { 28 | if (!io) { 29 | throw new Error('Socket.io not initialized!'); 30 | } 31 | return io; 32 | }; 33 | 34 | module.exports = { init, getIo }; -------------------------------------------------------------------------------- /setup_db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Enter your ElephantSQL database URI:" 4 | read DATABASE_URI 5 | 6 | echo "Applying schema to the database..." 7 | psql $DATABASE_URI < init.sql 8 | 9 | echo "Database setup complete." -------------------------------------------------------------------------------- /uri.txt: -------------------------------------------------------------------------------- 1 | { 2 | "results": { 3 | "command": "SELECT", 4 | "rowCount": 1, 5 | "oid": null, 6 | "rows": [ 7 | { 8 | "tables_with_columns": [ 9 | { 10 | "table_name": "vessels", 11 | "columns": [ 12 | "name", 13 | "cargo_capacity", 14 | "crew", 15 | "passengers", 16 | "vessel_type", 17 | "consumables", 18 | "vessel_class", 19 | "length", 20 | "cost_in_credits", 21 | "max_atmosphering_speed", 22 | "manufacturer", 23 | "_id", 24 | "model" 25 | ] 26 | }, 27 | { 28 | "table_name": "pg_stat_statements", 29 | "columns": [ 30 | "shared_blks_dirtied", 31 | "local_blks_written", 32 | "temp_blks_read", 33 | "temp_blks_written", 34 | "calls", 35 | "wal_fpi", 36 | "rows", 37 | "total_exec_time", 38 | "stddev_plan_time", 39 | "shared_blks_hit", 40 | "dbid", 41 | "mean_exec_time", 42 | "min_exec_time", 43 | "shared_blks_written", 44 | "plans", 45 | "shared_blks_read", 46 | "local_blks_dirtied", 47 | "wal_bytes", 48 | "max_plan_time", 49 | "blk_read_time", 50 | "wal_records", 51 | "total_plan_time", 52 | "blk_write_time", 53 | "stddev_exec_time", 54 | "local_blks_read", 55 | "query", 56 | "local_blks_hit", 57 | "userid", 58 | "queryid", 59 | "max_exec_time", 60 | "mean_plan_time", 61 | "min_plan_time" 62 | ] 63 | }, 64 | { 65 | "table_name": "species_in_films", 66 | "columns": [ 67 | "film_id", 68 | "species_id", 69 | "_id" 70 | ] 71 | }, 72 | { 73 | "table_name": "people_in_films", 74 | "columns": [ 75 | "person_id", 76 | "film_id", 77 | "_id" 78 | ] 79 | }, 80 | { 81 | "table_name": "species", 82 | "columns": [ 83 | "_id", 84 | "average_height", 85 | "language", 86 | "skin_colors", 87 | "classification", 88 | "eye_colors", 89 | "hair_colors", 90 | "average_lifespan", 91 | "homeworld_id", 92 | "name" 93 | ] 94 | }, 95 | { 96 | "table_name": "pilots", 97 | "columns": [ 98 | "vessel_id", 99 | "_id", 100 | "person_id" 101 | ] 102 | }, 103 | { 104 | "table_name": "starship_specs", 105 | "columns": [ 106 | "hyperdrive_rating", 107 | "MGLT", 108 | "vessel_id", 109 | "_id" 110 | ] 111 | }, 112 | { 113 | "table_name": "planets", 114 | "columns": [ 115 | "orbital_period", 116 | "_id", 117 | "rotation_period", 118 | "gravity", 119 | "surface_water", 120 | "terrain", 121 | "climate", 122 | "diameter", 123 | "name", 124 | "population" 125 | ] 126 | }, 127 | { 128 | "table_name": "films", 129 | "columns": [ 130 | "producer", 131 | "episode_id", 132 | "release_date", 133 | "opening_crawl", 134 | "_id", 135 | "title", 136 | "director" 137 | ] 138 | }, 139 | { 140 | "table_name": "people", 141 | "columns": [ 142 | "skin_color", 143 | "height", 144 | "homeworld_id", 145 | "name", 146 | "mass", 147 | "species_id", 148 | "birth_year", 149 | "hair_color", 150 | "_id", 151 | "eye_color", 152 | "gender" 153 | ] 154 | }, 155 | { 156 | "table_name": "planets_in_films", 157 | "columns": [ 158 | "film_id", 159 | "_id", 160 | "planet_id" 161 | ] 162 | }, 163 | { 164 | "table_name": "vessels_in_films", 165 | "columns": [ 166 | "film_id", 167 | "_id", 168 | "vessel_id" 169 | ] 170 | } 171 | ] 172 | } 173 | ], 174 | "fields": [ 175 | { 176 | "name": "tables_with_columns", 177 | "tableID": 0, 178 | "columnID": 0, 179 | "dataTypeID": 114, 180 | "dataTypeSize": -1, 181 | "dataTypeModifier": -1, 182 | "format": "text" 183 | } 184 | ], 185 | "_parsers": [ 186 | null 187 | ], 188 | "_types": { 189 | "_types": { 190 | "arrayParser": {}, 191 | "builtins": { 192 | "BOOL": 16, 193 | "BYTEA": 17, 194 | "CHAR": 18, 195 | "INT8": 20, 196 | "INT2": 21, 197 | "INT4": 23, 198 | "REGPROC": 24, 199 | "TEXT": 25, 200 | "OID": 26, 201 | "TID": 27, 202 | "XID": 28, 203 | "CID": 29, 204 | "JSON": 114, 205 | "XML": 142, 206 | "PG_NODE_TREE": 194, 207 | "SMGR": 210, 208 | "PATH": 602, 209 | "POLYGON": 604, 210 | "CIDR": 650, 211 | "FLOAT4": 700, 212 | "FLOAT8": 701, 213 | "ABSTIME": 702, 214 | "RELTIME": 703, 215 | "TINTERVAL": 704, 216 | "CIRCLE": 718, 217 | "MACADDR8": 774, 218 | "MONEY": 790, 219 | "MACADDR": 829, 220 | "INET": 869, 221 | "ACLITEM": 1033, 222 | "BPCHAR": 1042, 223 | "VARCHAR": 1043, 224 | "DATE": 1082, 225 | "TIME": 1083, 226 | "TIMESTAMP": 1114, 227 | "TIMESTAMPTZ": 1184, 228 | "INTERVAL": 1186, 229 | "TIMETZ": 1266, 230 | "BIT": 1560, 231 | "VARBIT": 1562, 232 | "NUMERIC": 1700, 233 | "REFCURSOR": 1790, 234 | "REGPROCEDURE": 2202, 235 | "REGOPER": 2203, 236 | "REGOPERATOR": 2204, 237 | "REGCLASS": 2205, 238 | "REGTYPE": 2206, 239 | "UUID": 2950, 240 | "TXID_SNAPSHOT": 2970, 241 | "PG_LSN": 3220, 242 | "PG_NDISTINCT": 3361, 243 | "PG_DEPENDENCIES": 3402, 244 | "TSVECTOR": 3614, 245 | "TSQUERY": 3615, 246 | "GTSVECTOR": 3642, 247 | "REGCONFIG": 3734, 248 | "REGDICTIONARY": 3769, 249 | "JSONB": 3802, 250 | "REGNAMESPACE": 4089, 251 | "REGROLE": 4096 252 | } 253 | }, 254 | "text": {}, 255 | "binary": {} 256 | }, 257 | "RowCtor": null, 258 | "rowAsArray": false, 259 | "_prebuiltEmptyResultObject": { 260 | "tables_with_columns": null 261 | } 262 | } 263 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | // mode: "production", 6 | mode: process.env.NODE_ENV, 7 | entry: './client/index.js', 8 | output: { 9 | filename: 'bundle.js', 10 | path: path.resolve(__dirname, 'build'), 11 | publicPath: '/', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.jsx?/, 17 | exclude: /node_modules/, 18 | use: { 19 | loader: 'babel-loader', 20 | options: { 21 | presets: [ 22 | ['@babel/env', { targets: 'defaults' }], 23 | ['@babel/react'], 24 | ], 25 | }, 26 | }, 27 | }, 28 | { 29 | test: /\.(scss|css)$/, 30 | use: ['style-loader', 'css-loader', 'sass-loader'], 31 | }, 32 | { 33 | test: /\.(png|jpg|jpeg|gif|svg|cur)$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'url-loader', 38 | options: { 39 | limit: 8192, 40 | name: 'images/[name].[hash:8].[ext]', 41 | }, 42 | }, 43 | ], 44 | }, 45 | ], 46 | }, 47 | plugins: [ 48 | new HtmlWebpackPlugin({ 49 | title: 'development', 50 | template: 'client/index.html', 51 | publicPath: '/', 52 | }), 53 | ], 54 | devServer: { 55 | historyApiFallback: true, 56 | hot: true, 57 | // disables the full-screen overlay that displays build errors 58 | // uncomment while styling 59 | client: { 60 | overlay: false, 61 | }, 62 | 63 | static: { 64 | publicPath: '/build', 65 | directory: path.join(__dirname, 'build'), 66 | }, 67 | proxy: { '/': 'http://localhost:3000' }, //added this to do postman requests 68 | port: 8080, 69 | }, 70 | resolve: { 71 | extensions: ['.js', '.jsx'], 72 | }, 73 | }; --------------------------------------------------------------------------------