├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------