├── .eslintrc.json ├── .gitignore ├── README.md ├── database ├── schemas.sql └── tables.sql ├── index.html ├── package.json ├── server.js ├── src ├── App.jsx ├── Buttons.jsx ├── Flow.jsx ├── Login.jsx ├── custom_nodes │ └── parent_node.js ├── index.js └── styles.css ├── users.js └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "airbnb" 11 | ], 12 | "overrides": [ 13 | { 14 | "files": [ 15 | "*.js" 16 | ], 17 | "rules": { 18 | "react/jsx-filename-extension": "off" 19 | } 20 | } 21 | ], 22 | "parserOptions": { 23 | "ecmaVersion": "latest", 24 | "ecmaFeatures": { 25 | "jsx": true 26 | }, 27 | "sourceType": "module" 28 | }, 29 | "plugins": [ 30 | "react" 31 | ], 32 | "globals": { 33 | "jwt": true 34 | }, 35 | "rules": { 36 | "no-console": "off", 37 | "import/no-extraneous-dependencies": [ 38 | "error", 39 | { 40 | "devDependencies": true 41 | } 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | dist/bundle.js 4 | package-lock.json 5 | .env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQL Schema Visualizer 2 | -------------------------------------------------------------------------------- /database/schemas.sql: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csuzukida/SQL-Schema-Visualizer/7cc977f1c6735c8948e9f4623d45ec1c8b2b9b74/database/schemas.sql -------------------------------------------------------------------------------- /database/tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS people ( 2 | _id BIGSERIAL PRIMARY KEY, 3 | name VARCHAR(50) NOT NULL, 4 | mass VARCHAR(50), 5 | hair_color VARCHAR(50), 6 | skin_color VARCHAR(50), 7 | eye_color VARCHAR(50), 8 | birth_year VARCHAR(50), 9 | gender VARCHAR(50), 10 | species_id BIGINT REFERENCES species(_id), 11 | homeworld_id BIGINT REFERENCES planets(_id), 12 | height INTEGER 13 | ); 14 | 15 | CREATE TABLE IF NOT EXISTS species ( 16 | _id BIGSERIAL PRIMARY KEY, 17 | name VARCHAR(50) NOT NULL, 18 | classification VARCHAR(50), 19 | average_height VARCHAR(50), 20 | average_lifespan VARCHAR(50), 21 | hair_colors VARCHAR(50), 22 | skin_colors VARCHAR(50), 23 | eye_colors VARCHAR(50), 24 | language VARCHAR(50), 25 | homeworld_id BIGINT REFERENCES planets(_id) 26 | ); 27 | 28 | CREATE TABLE IF NOT EXISTS planets ( 29 | _id BIGSERIAL PRIMARY KEY, 30 | name VARCHAR(50), 31 | rotation_period INTEGER, 32 | diameter INTEGER, 33 | orbital_period INTEGER, 34 | climate VARCHAR(50), 35 | gravity VARCHAR(50), 36 | terrain VARCHAR(50), 37 | surface_water VARCHAR(50), 38 | population VARCHAR(50) 39 | ); 40 | 41 | CREATE TABLE IF NOT EXISTS vessels ( 42 | _id BIGSERIAL PRIMARY KEY, 43 | name VARCHAR(50) NOT NULL, 44 | manufacturer VARCHAR(50), 45 | model VARCHAR(50), 46 | vessel_type VARCHAR(50) NOT NULL, 47 | vessel_class VARCHAR(50) NOT NULL, 48 | cost_in_credits BIGINT, 49 | length VARCHAR(50), 50 | max_atmosphering_speed VARCHAR(50), 51 | crew INTEGER, 52 | passengers INTEGER, 53 | cargo_capacity VARCHAR(50), 54 | consumables VARCHAR(50) 55 | ); 56 | 57 | CREATE TABLE IF NOT EXISTS starships_specs ( 58 | _id BIGSERIAL PRIMARY KEY, 59 | hyperdrive_rating VARCHAR(50), 60 | MGLT VARCHAR(50), 61 | vessel_id BIGINT REFERENCES vessels(_id) 62 | ); 63 | 64 | CREATE TABLE IF NOT EXISTS films ( 65 | _id BIGSERIAL PRIMARY KEY, 66 | title VARCHAR(50) NOT NULL, 67 | episode_id INTEGER NOT NULL, 68 | opening_crawl VARCHAR(500) NOT NULL, 69 | director VARCHAR(50) NOT NULL, 70 | producer VARCHAR(50) NOT NULL, 71 | release_date DATE NOT NULL 72 | ); 73 | 74 | CREATE TABLE IF NOT EXISTS pilots ( 75 | _id BIGSERIAL PRIMARY KEY, 76 | person_id BIGINT REFERENCES people(_id), 77 | vessel_id BIGINT REFERENCES vessels(_id) 78 | ); 79 | 80 | CREATE TABLE IF NOT EXISTS people_in_films ( 81 | _id BIGSERIAL PRIMARY KEY, 82 | person_id BIGINT REFERENCES people(_id), 83 | film_id BIGINT REFERENCES films(_id) 84 | ); 85 | 86 | CREATE TABLE IF NOT EXISTS species_in_films ( 87 | _id BIGSERIAL PRIMARY KEY, 88 | film_id BIGINT REFERENCES films(_id), 89 | species_id BIGINT REFERENCES species(_id) 90 | ); 91 | 92 | CREATE TABLE IF NOT EXISTS planets_in_films ( 93 | _id BIGSERIAL PRIMARY KEY, 94 | film_id BIGINT REFERENCES films(_id), 95 | planet_id BIGINT REFERENCES planets(_id) 96 | ); 97 | 98 | CREATE TABLE IF NOT EXISTS vessels_in_films ( 99 | _id BIGSERIAL PRIMARY KEY, 100 | film_id BIGINT REFERENCES films(_id), 101 | vessel_id BIGINT REFERENCES vessels(_id) 102 | ); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | SQL Schema Visualizer 12 | 13 | 14 |

SQL Diagram Visualizer

15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql_schema_visualizer", 3 | "version": "1.0.0", 4 | "description": "SQL Schema Visualizer", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack & nodemon server.js", 8 | "build": "webpack --mode production", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/csuzukida/SQL-Schema-Visualizer.git" 14 | }, 15 | "author": "Chris Suzukida", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/csuzukida/SQL-Schema-Visualizer/issues" 19 | }, 20 | "homepage": "https://github.com/csuzukida/SQL-Schema-Visualizer#readme", 21 | "dependencies": { 22 | "axios": "^1.3.4", 23 | "dotenv": "^16.0.3", 24 | "express": "^4.18.2", 25 | "fs": "^0.0.1-security", 26 | "jsonwebtoken": "^9.0.0", 27 | "pg": "^8.9.0", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-flow-renderer": "^10.3.17", 31 | "react-redux": "^8.0.5", 32 | "react-router": "^6.8.1", 33 | "reactflow": "^11.5.6" 34 | }, 35 | "devDependencies": { 36 | "@babel/core": "^7.21.0", 37 | "@babel/preset-env": "^7.20.2", 38 | "@babel/preset-react": "^7.18.6", 39 | "babel-loader": "^9.1.2", 40 | "css-loader": "^6.7.3", 41 | "eslint": "^8.34.0", 42 | "eslint-config-airbnb": "^19.0.4", 43 | "eslint-plugin-import": "^2.27.5", 44 | "eslint-plugin-jsx-a11y": "^6.7.1", 45 | "eslint-plugin-react": "^7.32.2", 46 | "eslint-plugin-react-hooks": "^4.6.0", 47 | "html-webpack-plugin": "^5.5.0", 48 | "jsonwebtoken": "^9.0.0", 49 | "nodemon": "^2.0.20", 50 | "source-map-loader": "^4.0.1", 51 | "style-loader": "^3.3.1", 52 | "webpack": "^5.75.0", 53 | "webpack-cli": "^5.0.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | require('dotenv').config(); 3 | 4 | const express = require('express'); 5 | const path = require('path'); 6 | const { Pool } = require('pg'); 7 | const fs = require('fs'); 8 | 9 | const dbPassword = process.env.DB_PASSWORD; 10 | 11 | const app = express(); 12 | 13 | const pool = new Pool({ 14 | user: 'postgres', 15 | host: 'localhost', 16 | database: 'SQL Test', 17 | password: dbPassword, 18 | port: 5432, 19 | }); 20 | 21 | app.use(express.json()); 22 | app.use( 23 | '/static', 24 | express.static(path.join(__dirname, 'dist'), { 25 | // Disable compression for CSS files and serve .map files 26 | setHeaders: (res, p) => { 27 | if (p.endsWith('.css')) { 28 | res.setHeader('Content-Encoding', 'identity'); 29 | } 30 | if (p.endsWith('.map')) { 31 | res.setHeader('Content-Type', 'application/json'); 32 | } 33 | }, 34 | }), 35 | ); 36 | app.use( 37 | '/flow', 38 | express.static(path.join(__dirname, 'node_modules', 'react-flow-renderer', 'dist', 'esm')), 39 | ); 40 | 41 | app.get('/', (req, res) => { 42 | res.set('Content-Type', 'text/html'); 43 | res.sendFile(path.join(__dirname, 'index.html')); 44 | }); 45 | 46 | app.get('/styles.css', (req, res) => { 47 | res.set('Content-Type', 'text/css'); 48 | res.sendFile(path.join(__dirname, 'src', 'styles.css')); 49 | }); 50 | 51 | app.get('/bundle.js', (req, res) => { 52 | res.set('Content-Type', 'application/javascript'); 53 | res.sendFile(path.join(__dirname, 'dist', 'bundle.js')); 54 | }); 55 | 56 | app.get('/api/schemas', async (req, res) => { 57 | try { 58 | const publicSchemaQuery = ` 59 | SELECT 60 | tc.table_name, 61 | kcu.column_name, 62 | ccu.table_name AS foreign_table_name, 63 | ccu.column_name AS foreign_column_name 64 | FROM 65 | information_schema.table_constraints AS tc 66 | JOIN information_schema.key_column_usage AS kcu 67 | ON tc.constraint_name = kcu.constraint_name 68 | JOIN information_schema.constraint_column_usage AS ccu 69 | ON ccu.constraint_name = tc.constraint_name 70 | LEFT JOIN information_schema.tables AS t 71 | ON t.table_name = ccu.table_name 72 | WHERE constraint_type = 'FOREIGN KEY' 73 | AND tc.table_schema = 'public'; 74 | `; 75 | 76 | const allTablesQuery = ` 77 | SELECT table_name 78 | FROM information_schema.tables 79 | WHERE table_schema = 'public' 80 | `; 81 | 82 | const client = await pool.connect(); 83 | const tablesWithForeignKeys = await client.query(publicSchemaQuery); 84 | const allTables = await client.query(allTablesQuery); 85 | 86 | const tablesWithForeignKeysFormatted = tablesWithForeignKeys.rows.map((row) => ({ 87 | table_name: row.table_name, 88 | id: '_id', 89 | foreign_key: row.column_name || null, 90 | references: row.foreign_table_name === row.table_name ? null : row.foreign_table_name, 91 | referenced_key_name: row.foreign_column_name || null, 92 | })); 93 | 94 | const allTablesFormatted = allTables.rows.map((row) => ({ 95 | table_name: row.table_name, 96 | id: '_id', 97 | foreign_key: null, 98 | references: null, 99 | referenced_key_name: null, 100 | })); 101 | 102 | const result = [ 103 | ...tablesWithForeignKeysFormatted, 104 | ...allTablesFormatted.filter( 105 | (table) => !tablesWithForeignKeysFormatted.some( 106 | (tableWithForeignKey) => tableWithForeignKey.table_name === table.table_name, 107 | ), 108 | ), 109 | ]; 110 | 111 | res.json({ result }); 112 | client.release(); 113 | } catch (err) { 114 | console.log(err); 115 | res.status(500).send('Error getting schemas'); 116 | } 117 | }); 118 | 119 | app.get('/flow/:file', (req, res) => { 120 | const { file } = req.params; 121 | const mapFile = path.join( 122 | __dirname, 123 | 'node_modules', 124 | 'react-flow-renderer', 125 | 'dist', 126 | 'esm', 127 | `${file}.js.map`, 128 | ); 129 | if (!fs.existsSync(mapFile)) { 130 | res.status(404).send(`Source map not found for ${file}`); 131 | } else { 132 | res.set('Content-Type', 'application/json'); 133 | res.sendFile(mapFile); 134 | } 135 | }); 136 | 137 | app.listen(3000, () => { 138 | console.log('Server is running on port 3000'); 139 | }); 140 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Flow from './Flow'; 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /src/Buttons.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | function Buttons(props) { 4 | const [showMenu, setShowMenu] = useState(false); 5 | const [nodeType, setNodeType] = useState('group'); 6 | const [nodeName, setNodeName] = useState(''); 7 | 8 | const handleSubmit = (event) => { 9 | event.preventDefault(); 10 | if (nodeType === 'group' && nodeName.trim()) { 11 | props.onAddNode(nodeName); 12 | } 13 | setShowMenu(false); 14 | setNodeType('group'); 15 | setNodeName(''); 16 | }; 17 | 18 | const handleMenuClick = () => { 19 | setShowMenu(true); 20 | }; 21 | 22 | const handleRadioChange = (e) => setNodeType(e.target.value); 23 | const handleNameChange = (e) => setNodeName(e.target.value); 24 | 25 | return ( 26 |
27 | 30 | {showMenu && ( 31 |
32 | 41 | {nodeType === 'group' && ( 42 | 43 | )} 44 | 45 |
46 | )} 47 |
48 | ); 49 | } 50 | 51 | export default Buttons; 52 | -------------------------------------------------------------------------------- /src/Flow.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import React, { useState, useCallback, useEffect } from 'react'; 3 | import ReactFlow, { 4 | addEdge, 5 | applyNodeChanges, 6 | applyEdgeChanges, 7 | Controls, 8 | Background, 9 | DefaultEdgeOptions, 10 | } from 'react-flow-renderer'; 11 | import axios from 'axios'; 12 | import Buttons from './Buttons'; 13 | import 'react-flow-renderer/dist/style.css'; 14 | 15 | function Flow() { 16 | const [nodes, setNodes] = useState([]); 17 | const [edges, setEdges] = useState([]); 18 | const [showModal, setShowModal] = useState(false); 19 | const [nodeName, setNodeName] = useState(''); 20 | const [selectedNode, setSelectedNode] = useState(null); 21 | 22 | const onNodesChange = useCallback( 23 | (changes) => setNodes((nds) => applyNodeChanges(changes, nds)), 24 | [], 25 | ); 26 | 27 | const onEdgesChange = useCallback( 28 | (changes) => setEdges((eds) => applyEdgeChanges(changes, eds)), 29 | [], 30 | ); 31 | 32 | const onConnect = useCallback( 33 | (params) => setEdges((eds) => addEdge({ ...params, sourceHandle: null, targetHandle: 'target' }, eds)), 34 | [], 35 | ); 36 | 37 | const handleAddForeignKey = (foreignKeyName) => { 38 | const selectedParentNode = selectedNode.id; 39 | console.log('selectedParentNode', selectedParentNode); 40 | const newForeignKeyNode = { 41 | id: foreignKeyName + 2, 42 | type: 'input', 43 | data: { label: `${foreignKeyName}` }, 44 | position: { x: 25, y: 45 }, 45 | parentNode: selectedParentNode, 46 | style: { 47 | background: '#c89666', 48 | color: '#12343b', 49 | }, 50 | zIndex: 10, 51 | sourcePosition: 'right', 52 | extent: 'parent', 53 | }; 54 | setNodes([...nodes, newForeignKeyNode]); 55 | setShowModal(false); 56 | }; 57 | 58 | const handleSubmit = (event) => { 59 | event.preventDefault(); 60 | handleAddForeignKey(nodeName); 61 | }; 62 | 63 | const handleDoubleClick = (event, node) => { 64 | console.log('double click'); 65 | event.preventDefault(); 66 | if (node.type === 'group') { 67 | setSelectedNode(node); 68 | setShowModal(true); 69 | } 70 | }; 71 | 72 | const handleAddNode = (nodeName) => { 73 | const colWidth = 300; 74 | const rowHeight = 250; 75 | 76 | const lastNode = nodes[nodes.length - 1]; 77 | const lastNodeX = lastNode.position.x; 78 | const lastNodeY = lastNode.position.y; 79 | 80 | const newNodeX = lastNodeX + colWidth; 81 | const newNodeY = lastNodeY + rowHeight; 82 | 83 | const newGroupNode = { 84 | id: nodeName, 85 | type: 'group', 86 | position: { 87 | x: newNodeX + Math.floor(Math.random() * 10), 88 | y: newNodeY + Math.floor(Math.random() * 10), 89 | }, 90 | style: { 91 | background: '#2d545e', 92 | border: '1px solid #E2BAB1', 93 | padding: 10, 94 | borderRadius: 5, 95 | width: 200, 96 | height: 150, 97 | }, 98 | }; 99 | 100 | const newTableNode = { 101 | id: nodeName + 1, 102 | type: 'output', 103 | data: { 104 | label: `Table ${nodeName}`, 105 | }, 106 | position: { x: 25, y: 10 }, 107 | parentNode: nodeName, 108 | extent: 'parent', 109 | style: { 110 | background: '#c89666', 111 | color: '#12343b', 112 | fontWeight: 'bold', 113 | }, 114 | targetPosition: 'left', 115 | }; 116 | 117 | setNodes([newGroupNode, newTableNode, ...nodes]); 118 | }; 119 | 120 | useEffect(() => { 121 | async function fetchData() { 122 | try { 123 | const response = await axios.get('/api/schemas'); 124 | const schemaData = response.data.result; 125 | const allNodes = []; 126 | 127 | const numCols = 5; 128 | 129 | const startX = 100; 130 | const startY = 100; 131 | 132 | const colWidth = 300; 133 | const rowHeight = 250; 134 | 135 | // Generate the group nodes 136 | schemaData.forEach((schema, index) => { 137 | const col = index % numCols; 138 | const row = Math.floor(index / numCols); 139 | 140 | const xPos = startX + col * colWidth; 141 | const yPos = startY + row * rowHeight; 142 | 143 | allNodes.push({ 144 | id: schema.table_name, 145 | type: 'group', 146 | position: { 147 | x: xPos + Math.floor(Math.random() * 10), 148 | y: yPos + Math.floor(Math.random() * 10), 149 | }, 150 | style: { 151 | background: '#2d545e', 152 | border: '1px solid #E2BAB1', 153 | padding: 10, 154 | borderRadius: 5, 155 | width: 200, 156 | height: 150, 157 | }, 158 | }); 159 | }); 160 | 161 | // Generate the table nodes as children of the group nodes 162 | schemaData.forEach((schema) => { 163 | if (!allNodes.includes(schema.table_name)) { 164 | allNodes.push({ 165 | id: schema.table_name + 1, 166 | type: 'output', 167 | data: { 168 | label: `Table ${schema.table_name}`, 169 | }, 170 | position: { x: 25, y: 10 }, 171 | parentNode: schema.table_name, 172 | extent: 'parent', 173 | style: { 174 | background: '#c89666', 175 | color: '#12343b', 176 | fontWeight: 'bold', 177 | }, 178 | targetPosition: 'left', 179 | }); 180 | } 181 | }); 182 | 183 | // Generate the foreign key nodes as children of the table nodes 184 | schemaData.forEach((schema) => { 185 | if (schema.foreign_key) { 186 | allNodes.push({ 187 | id: schema.table_name + 2, 188 | type: 'input', 189 | data: { label: `${schema.foreign_key}` }, 190 | position: { x: 25, y: 60 }, 191 | parentNode: schema.table_name, 192 | style: { 193 | background: '#c89666', 194 | color: '#12343b', 195 | }, 196 | sourcePosition: 'right', 197 | extent: 'parent', 198 | }); 199 | } 200 | }); 201 | setNodes(allNodes); 202 | 203 | // Generate the edges 204 | const allEdges = []; 205 | 206 | schemaData.forEach((schema) => { 207 | if (schema.references) { 208 | const edge = { 209 | id: `${schema.table_name}-${schema.references}`, 210 | source: schema.table_name + 2, 211 | target: schema.references + 1, 212 | animated: true, 213 | zIndex: 10, 214 | interactionWidth: 50, 215 | }; 216 | 217 | allEdges.push(edge); 218 | } 219 | }); 220 | 221 | setEdges(allEdges); 222 | } catch (err) { 223 | console.log(err); 224 | } 225 | } 226 | fetchData(); 227 | }, []); 228 | 229 | return ( 230 |
231 | 232 | 243 | 244 | 245 | 246 | {showModal && ( 247 |
248 |
249 | 253 | 254 |
255 |
256 | )} 257 |
258 | ); 259 | } 260 | 261 | export default Flow; 262 | -------------------------------------------------------------------------------- /src/Login.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | import React, { useState } from 'react'; 3 | 4 | function Login() { 5 | const [username, setUsername] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | 8 | const handleUsernameChange = (event) => { 9 | setUsername(event.target.value); 10 | }; 11 | 12 | const handlePasswordChange = (event) => { 13 | setPassword(event.target.value); 14 | }; 15 | 16 | const handleSubmit = async (event) => { 17 | event.preventDefault(); 18 | try { 19 | const response = await fetch('/api/login', { 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | }, 24 | body: JSON.stringify({ username, password }), 25 | }); 26 | 27 | if (response.ok) { 28 | const data = await response.json(); 29 | localStorage.setItem('token', data.token); 30 | console.log(`Logged in as ${data.username}`); 31 | } else { 32 | console.log('Login failed'); 33 | } 34 | } catch (error) { 35 | console.log({ message: 'Error in login', error }); 36 | } 37 | }; 38 | 39 | return ( 40 |
41 |
42 |
43 | 44 | 45 |
46 |
47 | 48 | 49 |
50 | 51 |
52 |
53 | ); 54 | } 55 | 56 | export default Login; 57 | -------------------------------------------------------------------------------- /src/custom_nodes/parent_node.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csuzukida/SQL-Schema-Visualizer/7cc977f1c6735c8948e9f4623d45ec1c8b2b9b74/src/custom_nodes/parent_node.js -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App'; 4 | 5 | createRoot(document.getElementById('root')).render(); 6 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | height: 100vh; 9 | font-family: Roboto, sans-serif; 10 | background-color: #12343b; 11 | color: #e1b382; 12 | } 13 | 14 | .container { 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | height: 90vh; 20 | width: 90vw; 21 | border: 1px solid black; 22 | margin-bottom: 1em; 23 | } 24 | 25 | .diagram { 26 | display: flex; 27 | flex-direction: column; 28 | justify-content: center; 29 | align-items: center; 30 | height: 100%; 31 | width: 100%; 32 | } 33 | 34 | .table-node { 35 | border: 1px solid #58aaaf; 36 | background-color: rgb(205, 186, 194); 37 | border-radius: 5px; 38 | height: 1.5rem; 39 | width: auto; 40 | padding: 1rem; 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | } 45 | 46 | .modal { 47 | position: fixed; 48 | z-index: 10; 49 | left: 0; 50 | top: 0; 51 | width: 100%; 52 | height: 100%; 53 | overflow: auto; 54 | background-color: rgba(0, 0, 0, 0.4); 55 | display: flex; 56 | align-items: center; 57 | justify-content: center; 58 | } 59 | 60 | .modal-form { 61 | background-color: #fefefe; 62 | margin: 15% auto; 63 | padding: 20px; 64 | border: 1px solid #888; 65 | width: 80%; 66 | } 67 | 68 | .modal-form label { 69 | display: block; 70 | margin-bottom: 0.5rem; 71 | } 72 | -------------------------------------------------------------------------------- /users.js: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { 3 | id: 1, 4 | username: 'user1', 5 | password: 'password1', 6 | }, 7 | { 8 | id: 2, 9 | username: 'user2', 10 | password: 'password2', 11 | }, 12 | ]; 13 | 14 | export default users; 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'development', 7 | entry: './src/index.js', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'bundle.js', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|jsx)$/, 16 | exclude: /node_modules/, 17 | use: { 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-env', '@babel/preset-react'], 21 | }, 22 | }, 23 | }, 24 | { 25 | test: /\.css$/i, 26 | use: ['style-loader', 'css-loader'], 27 | }, 28 | { 29 | enforce: 'pre', 30 | test: /\.js$/, 31 | loader: 'source-map-loader', 32 | }, 33 | ], 34 | }, 35 | plugins: [ 36 | new HtmlWebpackPlugin({ 37 | template: path.resolve(__dirname, 'index.html'), 38 | }), 39 | ], 40 | resolve: { 41 | extensions: ['.js', '.jsx', '.css'], 42 | }, 43 | devtool: 'source-map', 44 | watch: true, 45 | }; 46 | --------------------------------------------------------------------------------