├── .DS_Store ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── __tests__ ├── jest.config.js └── server_test │ ├── User.test.js │ ├── password.test.js │ └── supertest.test.js ├── app ├── .DS_Store ├── assets │ ├── LOGO.png │ ├── favicon.ico │ ├── red-x-icon.svg │ ├── success-green-check-mark-icon.svg │ └── warning-icon.svg ├── public │ └── index.html └── src │ ├── components │ ├── App.jsx │ ├── Authentication │ │ ├── Login.js │ │ └── SignUp.js │ ├── Charts │ │ ├── LineChart.jsx │ │ └── PieChart.jsx │ ├── ContainerComponent.jsx │ ├── Footers │ │ └── Footer.jsx │ ├── LoadingInformation │ │ └── LoadingInformationContainer.jsx │ ├── Managers │ │ ├── ContainerHealthLogs.jsx │ │ ├── HealthStatusDisplay.jsx │ │ └── ManagerMetricsContainer.jsx │ ├── Modal │ │ ├── Backdrop.jsx │ │ └── Modal.jsx │ ├── Navigation.jsx │ ├── TaskContainer.jsx │ ├── allTabs │ │ ├── firstTab.js │ │ └── secondTab.js │ ├── tabComponent │ │ ├── Loader.jsx │ │ └── Tabs.jsx │ └── tabNavAndContent │ │ ├── TabContent.jsx │ │ └── TabNavItem.jsx │ ├── index.css │ └── index.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── server ├── controllers │ ├── cookieController.js │ ├── dockerContainerController.js │ ├── dockerSwarmController.js │ ├── sessionController.js │ └── userController.js ├── helperFunctions │ ├── dockerCLI.js │ ├── dockerSwarmCLI.js │ ├── execProm.js │ └── parsers.js ├── models │ ├── containerSnapshotModel.js │ ├── sessionModel.js │ └── userModel.js ├── routes │ ├── dockerContainerRouter.js │ ├── dockerSwarmRouter.js │ └── user.js └── server.js ├── tailwind.config.js └── webpack.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Orcastration/6c71a2efcfcf4e1b60a04b9f78527bc3e6c7a83a/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Builds 2 | node_modules 3 | dist 4 | neighborModels.js 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 7 | 8 | license 9 | last-commit 10 |
11 |

Orcastration

12 |
13 |

14 | Orcastration is a Docker Swarm visualization tool, giving developers the power to view container metrics of their Docker Swarm with ease! A seamless and efficient GUI gives you insight to CPU usage, memory usage, NET I/O, and the health of each of your Docker swarm containers organized neatly by node and task. Say goodbye to clumsy and difficult to understand Docker CLI command outputs and say hello to Orcastration. 15 |

16 |
17 |

How it works

18 |

19 | Orcastration runs Docker CLI commands for you (out of sight and automatically) in order to retrieve Docker Swarm cluster information from the daemon. Data is then processed and graphs are generated in order to represent your various container metrics. Orcastration creates pie and line charts based on live-streamed data, so you can track your container's metrics in real-time. Orcastration also makes it easy to monitor the health and logs of your containers utilizing Docker Health Check. With the simple click of a button, get immediate feedback on the health status of your containers. 20 |

21 |

How to use Orcastration

22 |
23 |

First:

24 |
git clone https://github.com/oslabs-beta/Orcastration.git
25 | 26 |

Clone this repository to your machine.

27 |
28 |

Next:

29 |
30 |
npm install
31 |

Install dependencies in order to ensure proper app functionality.

32 |
33 |

Then:

34 |

35 | Confirm that your Docker Swarm and Docker Desktop are running. Verify that you are running Orcastration on the same machine that is hosting the manager node. The application must be running on the manager node’s machine in order to have the necessary access to the swarm's cluster management functionality. Please be aware that Orcastration runs on port 8080 and 3000, so be mindful that none of your containers share these ports! Also, understand that the Docker Health Check feature will only function for Docker containers configured with Docker healthcheck in the Dockerfile or the Docker Compose file. 36 |
37 |

Click here for more information on Docker Health Check. 38 |

39 |
40 |

Next:

41 |

42 | Orcastration utilizes a MongoDB database in order to efficiently serve your data. In order to ensure proper functionality, create a MongoDB database (click here for more information on setup). Then, create an .env file in the root of the Orcastration codebase and set an environment variable of 43 |
44 |

MONGO_URI
45 | to your newly created MongoDB URI.

Click here for more information on environment variables. 46 |

47 |
48 |

Finally...

49 |
npm run dev
50 |

51 | Run Orcastration and view your Docker Swarm metrics! (Note: if Orcastration does not run or you encounter errors, try restarting Docker Desktop!) 52 |

53 |
54 |

Want to contribute?

55 |

56 | Submit a pull request or reach out to one of our team members directly (contact information listed below). 57 |

58 |
59 |

Encounter a bug?

60 |

61 | Let us know! Submit an issue with the following format and we'll address it as soon as possible. 62 |
63 |
64 |

What is the bug?

65 |

How can you replicate the bug (please include specific steps)?

66 |

What is the severity of this bug: high (impacts core functionality), mid (slightly impacts functionality, but app still remains usable), or low (an annoyance)?

67 |

68 |
69 |

Contributors

70 | 117 | -------------------------------------------------------------------------------- /__tests__/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | testTimeout: 80000, 4 | }; 5 | -------------------------------------------------------------------------------- /__tests__/server_test/User.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const User = require('../../server/models/userModel'); 3 | const dotenv = require('dotenv').config({ path: './.env' }); 4 | 5 | const MONGO_URI = process.env.MONGO_URI; 6 | 7 | beforeAll(async () => { 8 | mongoose.connect(MONGO_URI, { 9 | useNewUrlParser: true, 10 | }); 11 | mongoose.connection.on('error', () => { 12 | throw new Error('cannot connect to database'); 13 | }); 14 | }); 15 | 16 | afterAll(async () => { 17 | try { 18 | await mongoose.connection.close(); 19 | } catch (err) { 20 | console.log(err); 21 | } 22 | }); 23 | 24 | describe('Signup', () => { 25 | beforeEach( 26 | async () => 27 | await new User({ 28 | email: 'createuser20@test.com', 29 | password: 'working', 30 | }).save() 31 | ); 32 | 33 | //find and delete the email created for verification to comply with 'unique' rule of MongoDB 34 | afterEach( 35 | async () => await User.findOneAndDelete({ email: 'createuser20@test.com' }) 36 | ); 37 | 38 | it('should create a new user', async () => { 39 | try { 40 | User.find({ email: 'createuser20@test.com' }).then((user) => 41 | expect(user[0].email).toBe('createuser20@test.com') 42 | ); 43 | } catch (err) { 44 | throw new Error(err); 45 | } 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /__tests__/server_test/password.test.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const User = require('../../server/models/userModel'); 3 | const bcrypt = require('bcryptjs'); 4 | const dotenv = require('dotenv').config({ path: './.env' }); 5 | 6 | const MONGO_URI = process.env.MONGO_URI; 7 | 8 | beforeAll(async () => { 9 | mongoose.connect(MONGO_URI, { 10 | useNewUrlParser: true, 11 | }); 12 | mongoose.connection.on('error', () => { 13 | throw new Error('cannot connect to database'); 14 | }); 15 | }); 16 | 17 | afterAll(async () => { 18 | try { 19 | await mongoose.connection.close(); 20 | } catch (err) { 21 | console.log(err); 22 | } 23 | }); 24 | 25 | describe('Login?', () => { 26 | beforeEach( 27 | async () => 28 | await new User({ 29 | email: 'testpassword@test.com', 30 | password: 'match', 31 | }).save() 32 | ); 33 | 34 | //find and delete the email created for verification to comply with 'unique' rule of MongoDB 35 | afterEach( 36 | async () => await User.findOneAndDelete({ email: 'testpassword@test.com' }) 37 | ); 38 | 39 | it('should throw an error if password is wrong', async () => { 40 | try { 41 | let userEmail = await User.find({ email: 'testpassword@test.com' }); 42 | console.log('userEmail', userEmail); 43 | let wrongPassword = 'abc'; 44 | //create userSchema.authenticate in userModels.js but it is not working 45 | const result = await bcrypt.compare(wrongPassword, userEmail[0].password); 46 | console.log('result', result); 47 | expect(result).toEqual(false); 48 | } catch (err) { 49 | throw new Error(err); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /__tests__/server_test/supertest.test.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const server = 'http://localhost:3000'; 3 | 4 | describe('Route integration', () => { 5 | describe('GET', () => { 6 | it('responds with 200 status code and json content type', () => { 7 | return request(server) 8 | .get('/dockerCont/getTasks') 9 | .expect('Content-Type', /json/) 10 | .expect(200); 11 | }); 12 | 13 | it('responds to invalid request with 400 status code and error message in body', () => { 14 | return request(server).get('/dockerCont/saveSwarmDat').expect(400); 15 | }); 16 | 17 | // it('responds with containers list', () => { 18 | // const response = request(server).get('/getContainers'); 19 | // expect(typeof response.body).toBe('array'); 20 | // }) 21 | }); 22 | 23 | describe('POST', () => { 24 | it('responds with 500 status code and json content type', () => { 25 | return request(server) 26 | .post('/dockerCont/saveSwarmData') 27 | .send({ UUID: '', containerList: '' }) 28 | .expect('Content-Type', /json/) 29 | .expect(500); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Orcastration/6c71a2efcfcf4e1b60a04b9f78527bc3e6c7a83a/app/.DS_Store -------------------------------------------------------------------------------- /app/assets/LOGO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Orcastration/6c71a2efcfcf4e1b60a04b9f78527bc3e6c7a83a/app/assets/LOGO.png -------------------------------------------------------------------------------- /app/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Orcastration/6c71a2efcfcf4e1b60a04b9f78527bc3e6c7a83a/app/assets/favicon.ico -------------------------------------------------------------------------------- /app/assets/red-x-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/success-green-check-mark-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/warning-icon.svg: -------------------------------------------------------------------------------- 1 | warning -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Orcastration 8 | 9 | 10 | 11 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Tabs from './tabComponent/Tabs'; 3 | import Navigation from './Navigation'; 4 | import ManagerMetricsContainer from './Managers/ManagerMetricsContainer'; 5 | import SignUp from './Authentication/SignUp'; 6 | import LogIn from './Authentication/Login'; 7 | import Loader from './tabComponent/Loader'; 8 | 9 | import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'; 10 | 11 | ChartJS.register(ArcElement, Tooltip, Legend); 12 | 13 | const App = (props) => { 14 | // const [signUp, setSignUp] = useState(true); 15 | // const [logIn, setLogIn] = useState(false); 16 | 17 | const [activeTab, setActiveTab] = useState('tab0'); 18 | const [currentStep, setCurrentStep] = useState('Starting'); 19 | const [currentNode, setCurrentNode] = useState(''); 20 | const [tasks, setTasks] = useState([]); 21 | const [loading, setLoading] = useState(false); 22 | const [healthStatus, setHealthStatus] = useState({ Status: 'waiting' }); 23 | 24 | const updateNode = (node) => { 25 | setCurrentNode(node); 26 | }; 27 | 28 | // const checkLogIn = () => { 29 | // const loggedInUser = localStorage.getItem('user'); 30 | 31 | // if (loggedInUser) { 32 | // setSignUp(false); 33 | // setLogIn(true); 34 | // } 35 | // }; 36 | 37 | // const signUpClick = () => { 38 | // const email = document.getElementById('email').value; 39 | // const password = document.getElementById('password').value; 40 | // //using the inputed email and password from the user, we send their credentials to our database to be stored 41 | // fetch(`http://localhost:3000/user/signup`, { 42 | // method: 'POST', 43 | // headers: { 44 | // Accept: 'application/json', 45 | // 'Content-Type': 'application/json', 46 | // }, 47 | // body: JSON.stringify({ email: email, password: password }), 48 | // }).then((data) => { 49 | // if (data.status === 200) { 50 | // //if post request is successful, we assign signUp to false and logIn to true 51 | // setSignUp(false); 52 | // setLogIn(true); 53 | // localStorage.setItem('user', true); 54 | // } else { 55 | // alert('The username has already been taken.'); 56 | // } 57 | // }); 58 | // }; 59 | 60 | // const logInClick = () => { 61 | // const email = document.getElementById('email').value; 62 | // const password = document.getElementById('password').value; 63 | // //using the inputed email and password provided by the user, we check to see if we have these credentials in the database 64 | // fetch('http://localhost:3000/user/login', { 65 | // method: 'POST', 66 | // headers: { 67 | // Accept: 'application/json', 68 | // 'Content-Type': 'application/json', 69 | // }, 70 | // body: JSON.stringify({ email: email, password: password }), 71 | // }).then((data) => { 72 | // if (data.status === 200) { 73 | // //if we confirm that this user has been signed up, we can allow them entry 74 | // //to the developer page (by setting signUp to false and login to true) 75 | // setSignUp(false); 76 | // setLogIn(true); 77 | // localStorage.setItem('user', true); 78 | // } 79 | // }); 80 | // }; 81 | 82 | // const logOutClick = () => { 83 | // setSignUp(true); 84 | // setLogIn(false); 85 | // localStorage.clear(); 86 | // }; 87 | 88 | // const logInPage = () => { 89 | // setSignUp(false); 90 | // }; 91 | 92 | // const signUpPage = () => { 93 | // setSignUp(true); 94 | // }; 95 | 96 | useEffect(() => { 97 | // checkLogIn(); 98 | setCurrentStep('IDs'); 99 | const fetchData = async () => { 100 | try { 101 | let rawData = await fetch('/dockerCont/getTasks'); 102 | let parsedData = await rawData.json(); 103 | //set setTasks to equal the result of submitting a get request to the above endpoint 104 | setTasks(parsedData); 105 | //set current node to equal the first node in the swarm 106 | setCurrentNode(parsedData[0].nodeID); 107 | setLoading(true); 108 | } catch (err) { 109 | console.log('Error in App.jsx useEffect', err); 110 | } 111 | }; 112 | fetchData(); 113 | }, []); 114 | 115 | // if (signUp === true) { 116 | // return ( 117 | //
118 | // 119 | //
120 | // ); 121 | // } else if (logIn === false) { 122 | // return ( 123 | //
124 | // 125 | //
126 | // ); 127 | // } else { 128 | return ( 129 |
130 | {/* */} 131 | 132 |
133 | 140 | {loading ? ( 141 | 150 | ) : ( 151 | 152 | )} 153 |
154 |
155 |
156 | ); 157 | // } 158 | }; 159 | 160 | export default App; 161 | -------------------------------------------------------------------------------- /app/src/components/Authentication/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LogIn = (props) => { 4 | return ( 5 |
6 |
7 | 10 | 17 |
18 |
19 |
Log In
20 |
21 |
email:
22 | 23 |
password:
24 | 25 |
26 | 33 |
34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default LogIn; 41 | -------------------------------------------------------------------------------- /app/src/components/Authentication/SignUp.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SignUp = (props) => { 4 | return ( 5 |
6 |
7 | 10 | 17 |
18 |
19 |
Create User
20 |
21 |
email:
22 | 23 |
password:
24 | 25 |
26 | 33 |
34 |
35 |
36 |
37 | ); 38 | }; 39 | 40 | export default SignUp; 41 | -------------------------------------------------------------------------------- /app/src/components/Charts/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import Chart from 'chart.js/auto'; 4 | import 'chartjs-adapter-luxon'; 5 | import ChartStreaming from 'chartjs-plugin-streaming'; 6 | Chart.register(ChartStreaming); 7 | 8 | export default function LineChart({ networkIO, change }) { 9 | // account for MB and GB as well 10 | const parseNetworkIO = (networkIO) => { 11 | const parsedNetworkIOList = networkIO.split('/'); 12 | const unitCovertedNetworkIOList = []; 13 | parsedNetworkIOList.forEach((dataPoint) => { 14 | if (dataPoint.includes('kB')) 15 | unitCovertedNetworkIOList.push(parseFloat(dataPoint)); 16 | else if (dataPoint.includes('MB')) 17 | unitCovertedNetworkIOList.push(parseFloat(dataPoint) * 1000); 18 | else if (dataPoint.includes('GB')) 19 | unitCovertedNetworkIOList.push(parseFloat(dataPoint) * 1000000); 20 | else unitCovertedNetworkIOList.push(parseFloat(dataPoint) / 1000); 21 | }); 22 | return unitCovertedNetworkIOList; 23 | }; 24 | 25 | // separate chartOptions for modularity 26 | const chartOptions = { 27 | plugins: { 28 | title: { 29 | display: true, 30 | text: `Network I/O: ${networkIO}`, 31 | }, 32 | legend: { 33 | display: true, 34 | position: 'bottom', 35 | }, 36 | streaming: { 37 | duration: 15000, 38 | }, 39 | }, 40 | tooltips: { 41 | enabled: true, 42 | mode: 'label', 43 | }, 44 | scales: { 45 | x: { 46 | type: 'realtime', 47 | realtime: { 48 | duration: 15000, 49 | frameRate: 20, 50 | delay: 1000, 51 | }, 52 | ticks: { color: 'rgba(4, 59, 92, 1)' }, 53 | grid: { 54 | color: 'white', 55 | lineWidth: 1, 56 | display: true, 57 | drawBorder: false, 58 | borderDash: [6], 59 | border: false, 60 | }, 61 | }, 62 | y: { 63 | ticks: { 64 | color: 'green', 65 | callback: function (value) { 66 | return value + ' kB'; 67 | }, 68 | }, 69 | grid: { 70 | color: 'white', 71 | lineWidth: 1, 72 | display: true, 73 | }, 74 | }, 75 | }, 76 | elements: { 77 | line: { 78 | spanGaps: true, 79 | }, 80 | }, 81 | animation: { duration: 0 }, 82 | }; 83 | 84 | // use useState and useEffect 85 | const [chartData, setChartData] = useState({ 86 | label: 'Network I/O', 87 | datasets: [ 88 | { 89 | label: 'Network Input', 90 | data: [], 91 | backgroundColor: 'rgba(83, 149, 238, 0.3)', 92 | borderColor: 'rgba(83, 149, 238, 0.8)', 93 | borderWidth: 1, 94 | fill: true, 95 | lineTension: 0.3, 96 | spanGaps: true, 97 | }, 98 | { 99 | label: 'Network Output', 100 | data: [], 101 | backgroundColor: 'rgba(229, 151, 47, 0.3)', 102 | borderColor: 'rgba(229, 151, 47, 0.8)', 103 | borderWidth: 1, 104 | fill: true, 105 | lineTension: 0.3, 106 | spanGaps: true, 107 | }, 108 | ], 109 | }); 110 | 111 | //useRef is not needed 112 | 113 | // use change as a dependency, since network i/o may be stagnant at times and will cause useEffect to not invoke 114 | useEffect(() => { 115 | const networkIODataPoints = parseNetworkIO(networkIO); 116 | // console.log('networkIODataPoints[0]', networkIODataPoints[0]) 117 | setChartData((prevChartData) => { 118 | return { 119 | ...prevChartData, 120 | datasets: [ 121 | { 122 | ...prevChartData.datasets[0], 123 | data: prevChartData.datasets[0].data.concat({ 124 | x: new Date(), 125 | y: networkIODataPoints[0], 126 | }), 127 | }, 128 | { 129 | ...prevChartData.datasets[1], 130 | data: prevChartData.datasets[1].data.concat({ 131 | x: new Date(), 132 | y: networkIODataPoints[1], 133 | }), 134 | }, 135 | ], 136 | }; 137 | }); 138 | }, [change]); 139 | 140 | return ( 141 |
142 | 143 |
144 | ); 145 | } 146 | 147 | // import React, { useEffect, useState, useRef } from 'react'; 148 | // // imports chart.js library 149 | // import Chart from 'chart.js/auto'; 150 | // // imports chartjs-adapter-luxon which allows us to use luxon library for date/time manipulation 151 | // import 'chartjs-adapter-luxon'; 152 | // // import line chart type 153 | // import { Line } from 'react-chartjs-2'; 154 | // // allows us to make real time updates to the chart 155 | // import ChartStreaming from 'chartjs-plugin-streaming'; 156 | // // tells chart.js to draw the line chart with gaps where there are empty or null values from data 157 | // Chart.overrides.line.spanGaps = false; 158 | // // overhead for letting user use the chartsreaming plugin we import on line 11 to allow for real time updates 159 | // Chart.register(ChartStreaming); 160 | 161 | // //refer to https://www.chartjs.org/docs/latest/charts/line.html - Chart.js Line Chart doc 162 | // export default function LineChart({ networkIO, change }) { 163 | // // console.log('propData', propData) // "1.5kB / 0B" 164 | // // console.log('change', change) // intialized to true 165 | // const chart = useRef(); 166 | 167 | // function setData(dataObj) { 168 | // // chart.current is abvailable from const chart = useRef() 169 | // // checking if chart.current is defined (returns undefined if not) and if so, accesses chart.current.data.datasets[0].data ([1.5]) and pushes an object to it 170 | // chart.current?.data.datasets[0].data.push({ 171 | // // for input 172 | // x: Number(dataObj.timestamp), // new Date() 173 | // y: dataObj.value1, // 1.5 174 | // }); // chart.current.data.datasets[0].data ([1.5, {x: 1/21/2023, y: 1.5}]) 175 | // chart.current?.data.datasets[1].data.push({ 176 | // // for output 177 | // x: Number(dataObj.timestamp), 178 | // y: dataObj.value2, 179 | // }); 180 | // // suppress any animation from the chart while its being updated 181 | // chart.current?.update('quiet'); 182 | // } 183 | 184 | // function dataSplit(string) { 185 | // if (!string) { 186 | // return []; 187 | // } 188 | // const result = []; 189 | // const stringArr = string.split(' / '); // changing string into an array => ['1.5kB', '0B'] 190 | // stringArr.forEach((el) => { 191 | // let numEl; 192 | // if (el.includes('kB')) { 193 | // numEl = parseFloat(el); 194 | // } else { 195 | // numEl = parseFloat(el) / 1000; 196 | // } 197 | // result.push(numEl); 198 | // }); 199 | // return result; 200 | // } 201 | 202 | // const dataArr = dataSplit(networkIO); // CONVERTING STRING I/O TO INTEGER I/O WITH SAME UNITS (kB), return sarray of length two => [1.5, 0] 203 | 204 | // const [chartData, setChartData] = useState({ 205 | // datasets: [ 206 | // { 207 | // label: ['Network Input'], 208 | // data: [dataArr[0]], // taking the input element => data: [1.5] 209 | // backgroundColor: 'rgba(229, 151, 47, 0.3)', 210 | // borderColor: 'rgba(229, 151, 47, 0.8)', 211 | // // fill: true, 212 | // lineTension: 0.3, 213 | // // spanGaps: true, 214 | // }, 215 | // { 216 | // label: ['Network Output'], 217 | // data: [dataArr[1]], // tkaing the output element => data: [0] 218 | // backgroundColor: 'rgba(83, 149, 238, 0.3)', 219 | // borderColor: 'rgba(83, 149, 238, 0.8)', 220 | // // fill: true, 221 | // lineTension: 0.3, 222 | // // spanGaps: true, 223 | // }, 224 | // ], 225 | // }); 226 | 227 | // useEffect(() => { 228 | // const newData = { 229 | // value1: dataArr[0], // [1.5] 230 | // value2: dataArr[1], // [0] 231 | // timestamp: new Date(), // integer for current data/time 232 | // }; 233 | // setData(newData); // updates chartData.datasets[0].data 234 | // setChartData((prevState) => ({ 235 | // ...prevState, 236 | // datasets: [ 237 | // { 238 | // ...prevState.datasets[0], 239 | // backgroundColor: 'rgba(229, 151, 47, 0.3)', 240 | // }, 241 | // { 242 | // ...prevState.datasets[1], 243 | // backgroundColor: 'rgba(83, 149, 238, 0.3)', 244 | // }, 245 | // ], 246 | // })); 247 | // }, [change]); 248 | 249 | // return ( 250 | //
251 | // 281 | //
282 | // ); 283 | // } 284 | -------------------------------------------------------------------------------- /app/src/components/Charts/PieChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Pie } from 'react-chartjs-2'; 3 | 4 | export default function PieChart({ perc, containerStat }) { 5 | const chartOptions = { 6 | plugins: { 7 | title: { 8 | display: true, 9 | text: 10 | containerStat === 'CPUPerc' 11 | ? `CPU Usage: ${perc}` 12 | : `MEM Usage: ${perc}`, 13 | }, 14 | legend: { 15 | display: true, 16 | position: 'right', 17 | align: 'center', 18 | labels: { 19 | padding: 10, 20 | }, 21 | }, 22 | borderWidth: 1, 23 | }, 24 | animation: { 25 | duration: 1500, 26 | }, 27 | maintainAspectRatio: false, 28 | }; 29 | 30 | // define chartData using useState 31 | const [chartData, setChartData] = useState({ 32 | labels: ['Usage', 'Free Space'], 33 | datasets: [ 34 | { 35 | label: 'Container Usage Ratio', 36 | data: [0, 0], 37 | backgroundColor: [ 38 | 'rgba(75,192,192,0.2)', 39 | 'rgba(36, 161, 252, 0.2)', 40 | ], 41 | borderColor: ['rgba(75,192,192,1)', '#19314D'], 42 | borderWidth: 1, 43 | // hoverOffset: 20, 44 | }, 45 | ], 46 | }); 47 | 48 | useEffect(() => { 49 | setChartData((prevChartData) => { 50 | return { 51 | ...prevChartData, 52 | datasets: [ 53 | { 54 | ...prevChartData.datasets[0], 55 | data: [parseFloat(perc), 100 - parseFloat(perc)], 56 | }, 57 | ], 58 | }; 59 | }); 60 | }, [perc]); 61 | 62 | return ( 63 |
64 | 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /app/src/components/ContainerComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import PieChart from './Charts/PieChart'; 4 | import LineChart from './Charts/LineChart'; 5 | 6 | export default function ContainerComponent({ 7 | containerData, 8 | containerID, 9 | change, 10 | setHealthStatus, 11 | }) { 12 | const [toggleData, setToggleData] = useState(false); 13 | const [toggleHealth, setToggleHealth] = useState(false); 14 | 15 | const handleClick = () => { 16 | healthCheck(containerID); 17 | }; 18 | 19 | const healthCheck = (containerID) => { 20 | fetch(`/dockerSwarm/getHealth/${containerID}`) 21 | .then((response) => response.json()) 22 | .then((res) => { 23 | //if container has not been set up with a docker health check file, it will return null and we will not be able to provide health check details 24 | if (res[0] === null) { 25 | setToggleHealth(true); 26 | setHealthStatus('null'); 27 | setToggleHealth((prev) => !prev); 28 | } else { 29 | //if container HAS been setup for docker health check, we can move forward with supplying this health check information 30 | setHealthStatus(Object.assign(res[0], { containerID: containerID })); 31 | } 32 | }); 33 | }; 34 | 35 | return ( 36 | 37 | 43 |
44 |
45 | {!containerData ? ( 46 | '' 47 | ) : ( 48 |

49 | Container Name: 50 | {containerData.Name} 51 |

52 | )} 53 |

54 | Container ID: {containerID} 55 |

56 |
57 | 63 |
64 | {containerData && ( 65 |
66 | 67 | 68 | 69 |
70 | )} 71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /app/src/components/Footers/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'React'; 2 | 3 | export default function Footer() { 4 | return ( 5 |
Here is the footer
6 | ) 7 | } -------------------------------------------------------------------------------- /app/src/components/LoadingInformation/LoadingInformationContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function LoadingInformation({ currentStep, setCurrentStep }) { 4 | console.log('currentStep', currentStep); 5 | const handleClick = () => { 6 | setCurrentStep('Start'); 7 | }; 8 | 9 | const loadingProgress = { 10 | Starting: 'Starting up App', 11 | IDs: 'Retrieving Docker Swarm Nodes', 12 | Snapshot: 'Creating Snapshot of Current Docker Swarm Configuration', 13 | Ready: ( 14 | 20 | ), 21 | Start: ( 22 |
23 | Streaming Docker Swarm Container Metrics   24 | 25 |   26 | 27 |
28 | 34 |
35 | ), 36 | Stop: ( 37 | 43 | ), 44 | }; 45 | 46 | return ( 47 |
48 | {currentStep !== 'Ready' && currentStep !== 'Start' ? ( 49 |

{loadingProgress[currentStep]}

50 | ) : ( 51 | loadingProgress[currentStep] 52 | )} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /app/src/components/Managers/ContainerHealthLogs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ContainerHealthLogs = ({ healthStatus }) => { 4 | const { containerID, Log, FailingStreak } = healthStatus; 5 | //declare containerLogs array to hold div elements for each of our logs contained in the Log state 6 | const containerLogs = []; 7 | 8 | let className = 'text-green-700'; 9 | if (Log) { 10 | for (let i = 0; i < Log.length; i++) { 11 | //for each log within the Log state, we will create a div element that contains the log start, end, and exit code 12 | containerLogs.push( 13 |
14 | Start: {Log[i].Start} 15 |
16 | End: {Log[i].End} 17 |
18 | Exit Code: {Log[i].ExitCode} 19 |
20 |
21 |
22 | ); 23 | } 24 | // if log is defined, change the color of the exit codes 25 | if (Log[Log.length - 1].ExitCode === 1) { 26 | className = 'text-red-700'; 27 | } 28 | } 29 | 30 | return ( 31 |
32 |

33 | Viewing Container:
34 | Container ID: {containerID ? containerID.substring(0, 12) : null} 35 |
36 |

37 |
38 | FailingStreak:{' '} 39 | {FailingStreak} 40 |

Logs:

41 | {containerLogs} 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default ContainerHealthLogs; 48 | -------------------------------------------------------------------------------- /app/src/components/Managers/HealthStatusDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CheckMark from '../../../assets/success-green-check-mark-icon.svg'; 3 | import RedX from '../../../assets/red-x-icon.svg'; 4 | import Warning from '../../../assets/warning-icon.svg'; 5 | 6 | export default function HealthStatusDisplay({ healthStatus }) { 7 | const healthyOutput = { 8 | waiting: ( 9 |

10 | Click "Check Health" to display a container's health status. 11 |

12 | ), 13 | // its status says "starting", but its actually going through the health check test and failing the test (failstreak) 14 | // it will test for x amt of times (however your healthcheck is configured) until the container is classified as unhealthy 15 | starting: ( 16 |

17 | The container is attempting to start up. See terminal above to monitor 18 | progress. 19 |

20 | ), 21 | healthy: ( 22 |
23 |

24 | Healthy 25 |

26 | 27 |
28 | ), 29 | unhealthy: ( 30 |
31 |

32 | Unhealthy 33 |

34 | 35 |
36 | ), 37 | null: ( 38 |
39 | 40 | 41 | Health Check is not configured for the container. 42 | 43 |
44 | ), 45 | }; 46 | 47 | return ( 48 |
49 | {healthStatus ? healthyOutput[healthStatus] : healthyOutput.null} 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /app/src/components/Managers/ManagerMetricsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContainerHealthLogs from './ContainerHealthLogs.jsx'; 3 | import LoadingInformationContainer from '../LoadingInformation/LoadingInformationContainer'; 4 | import HealthStatusDisplay from './HealthStatusDisplay.jsx'; 5 | 6 | const ManagerMetricsContainer = ({ 7 | currentStep, 8 | setCurrentStep, 9 | healthStatus, 10 | }) => { 11 | return ( 12 |
16 | 17 | 18 | 22 |
23 | ); 24 | }; 25 | 26 | export default ManagerMetricsContainer; 27 | -------------------------------------------------------------------------------- /app/src/components/Modal/Backdrop.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | 3 | export default function Backdrop({ children, onClick }) { 4 | return ( 5 | 11 | {children} 12 | 13 | ); 14 | }; -------------------------------------------------------------------------------- /app/src/components/Modal/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import Backdrop from "./backdrop"; 3 | 4 | export default function Modal({ handleClose, text }) { 5 | return ( 6 | 7 | e.stopPropagation()} 9 | 10 | > 11 | 12 | 13 | 14 | ) 15 | } -------------------------------------------------------------------------------- /app/src/components/Navigation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../../../app/assets/LOGO.png'; 3 | 4 | const Navigation = (props) => { 5 | return ( 6 |
7 | 8 | 11 | {/* */} 18 |
19 | ); 20 | }; 21 | 22 | export default Navigation; 23 | -------------------------------------------------------------------------------- /app/src/components/TaskContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContainerComponent from './ContainerComponent'; 3 | import Loader from './tabComponent/Loader'; 4 | 5 | export default function TaskContainer({ 6 | id, 7 | containers, 8 | containerData, 9 | change, 10 | setHealthStatus, 11 | }) { 12 | let containerComponents = []; 13 | for (let i = 0; i < containers.length; i++) { 14 | //for every container available, we want to create a ContainerComponent passing down the containerID, and container 15 | //metric data 16 | containerComponents.push( 17 | 25 | ); 26 | } 27 | return ( 28 |
29 | 30 | Task ID: {id ? id : 'Loading Task'} 31 | 32 | 33 | {!containers.length ? null : containerComponents} 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/src/components/allTabs/firstTab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FirstTab = () => { 4 | return ( 5 |
6 |

First Tab Content

7 |
8 | ); 9 | }; 10 | 11 | export default FirstTab; 12 | -------------------------------------------------------------------------------- /app/src/components/allTabs/secondTab.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SecondTab = () => { 4 | return ( 5 |
6 |

Second Tab Content

7 |
8 | ); 9 | }; 10 | 11 | export default SecondTab; 12 | -------------------------------------------------------------------------------- /app/src/components/tabComponent/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Loader() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /app/src/components/tabComponent/Tabs.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import TabNavItem from '../tabNavAndContent/TabNavItem'; 3 | import TabContent from '../tabNavAndContent/TabContent'; 4 | import Loader from './Loader'; 5 | 6 | const Tabs = ({ 7 | allTasks, 8 | activeTab, 9 | setActiveTab, 10 | currentNode, 11 | setCurrentNode, 12 | updateNode, 13 | userEmail, 14 | currentStep, 15 | setCurrentStep, 16 | setHealthStatus, 17 | }) => { 18 | const [data, setData] = useState(''); 19 | const [tabContentArr, setTabContentArr] = useState([]); 20 | const [UUID, setUUID] = useState(null); 21 | const [change, setChange] = useState(false); 22 | 23 | //declare variable tabNavArr and initialize to empty array 24 | const [test, setTest] = useState(true); 25 | let tabNavArr = []; 26 | //declare variable tabContentArr and initialzie to empty array 27 | let tabContent = []; 28 | 29 | //loop through incoming tasks (use foreach loop below? we want this to happen on page load so yes. or can we put this in a function and then call th function in the fetch) 30 | const createNavAndContent = () => { 31 | for (let i = 0; i < allTasks.length; i++) { 32 | tabNavArr.push( 33 | 43 | ); 44 | } 45 | return; 46 | }; 47 | createNavAndContent(); 48 | 49 | const createTabContent = () => { 50 | for (let i = 0; i < allTasks.length; i++) { 51 | tabContent.push( 52 | 61 | ); 62 | } 63 | return; 64 | }; 65 | 66 | useEffect(() => { 67 | const fetchData = async () => { 68 | const reqObj = []; 69 | allTasks.forEach((node) => { 70 | node.tasks.forEach((task) => { 71 | task.containers.forEach((container) => { 72 | //for each container in each task in each node returned from allTasks, we will push the data contained in container 73 | reqObj.push(container); 74 | }); 75 | }); 76 | }); 77 | 78 | try { 79 | setCurrentStep('Snapshot'); 80 | let response = await fetch('/dockerCont/saveSwarmData', { 81 | method: 'POST', 82 | mode: 'cors', 83 | headers: { 84 | 'Content-Type': 'application/json', 85 | }, 86 | body: JSON.stringify(reqObj), 87 | }); 88 | 89 | let newUUID = await response.json(); 90 | setUUID(newUUID); 91 | setCurrentStep('Ready'); 92 | } catch (err) { 93 | console.log('Error in Tabs.jsx useEffect', err); 94 | } 95 | }; 96 | fetchData(); 97 | }, []); 98 | 99 | createTabContent(); 100 | 101 | useEffect(() => { 102 | if (currentStep === 'Start') { 103 | const sse = new EventSource( 104 | `http://localhost:3000/dockerCont/streamSwarmStats/${UUID}` 105 | ); 106 | console.log('Started Streaming'); 107 | sse.onmessage = (event) => { 108 | const data = JSON.parse(event.data); 109 | setData(data); 110 | setChange((prev) => !prev); 111 | if (currentStep === 'Stop') { 112 | sse.close(); 113 | } 114 | }; 115 | 116 | sse.onerror = (err) => { 117 | console.log('see.error', err); 118 | return () => { 119 | sse.close(); 120 | }; 121 | }; 122 | 123 | return () => { 124 | sse.close(); 125 | }; 126 | } 127 | }, [currentStep]); 128 | 129 | return ( 130 |
131 |
    132 | {tabNavArr.length === 0 ? : tabNavArr} 133 |
134 | {tabNavArr.length === 0 ? : tabContent} 135 |
136 | ); 137 | }; 138 | export default Tabs; 139 | -------------------------------------------------------------------------------- /app/src/components/tabNavAndContent/TabContent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TaskContainer from '../TaskContainer'; 3 | 4 | const TabContent = ({ 5 | id, 6 | activeTab, 7 | tasks, 8 | containerData, 9 | change, 10 | setHealthStatus, 11 | }) => { 12 | // use for loop to loop over tasks array, 13 | // for each loop, we want to create a task container passing a props down 14 | //for taskID, container data, and containerID as well as other props that will be used later on 15 | const taskContainers = []; 16 | for (let i = 0; i < tasks.length; i++) { 17 | taskContainers.push( 18 | 26 | ); 27 | } 28 | return activeTab === id ? ( 29 |
30 | {taskContainers} 31 |
32 | ) : null; 33 | }; 34 | 35 | export default TabContent; 36 | -------------------------------------------------------------------------------- /app/src/components/tabNavAndContent/TabNavItem.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TabNavItem = ({ 4 | id, 5 | title, 6 | activeTab, 7 | setActiveTab, 8 | updateNode, 9 | setCurrentNode, 10 | }) => { 11 | const handleClick = (title) => { 12 | setActiveTab(id); 13 | //when you click a nav tab, you should update the currentManager 14 | //to equal the title of the nav item 15 | updateNode(title); 16 | }; 17 | 18 | return ( 19 |
  • handleClick(title)} 21 | className={ 22 | (activeTab === id ? 'active ' : '') + 23 | 'min-w-fit h-2 flex flex-col justify-center w-fit mx-auto p-2' 24 | } 25 | > 26 | {title} 27 |
  • 28 | ); 29 | }; 30 | 31 | export default TabNavItem; 32 | -------------------------------------------------------------------------------- /app/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | height: 100vh; 7 | height: 100dvh; 8 | /* width: 100vw; */ 9 | } 10 | 11 | .signUp { 12 | font-size: 25px; 13 | margin: 50px auto; 14 | background-color: #19314d; 15 | border-radius: 5px; 16 | box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, 17 | rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, 18 | rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px; 19 | padding: 20px 20px; 20 | max-width: 100%; 21 | width: 600px; 22 | } 23 | 24 | .signUpInput { 25 | display: flex; 26 | height: 2rem; 27 | } 28 | 29 | .signUpTitle { 30 | color: white; 31 | font-size: 1.5rem; 32 | } 33 | 34 | .signUpInput { 35 | border-radius: 5px; 36 | font-size: 1rem; 37 | } 38 | 39 | .enter { 40 | background-color: #4b84aa; 41 | color: white; 42 | padding: 12px; 43 | font-size: 16px; 44 | border: none; 45 | margin-right: 10px; 46 | margin-left: 15px; 47 | border-radius: 5px; 48 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 6px 6px rgba(0, 0, 0, 0.1); 49 | margin-bottom: 20px; 50 | } 51 | 52 | /* .dockerHealth { 53 | background-color: hsl(232, 43%, 27%); 54 | color: white; 55 | padding: 12px; 56 | font-size: 16px; 57 | border: none; 58 | margin-right: 10px; 59 | margin-left: 15px; 60 | border-radius: 5px; 61 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 6px 6px rgba(0, 0, 0, 0.1); 62 | margin-bottom: 20px; 63 | } */ 64 | 65 | /* .ping { */ 66 | /* width: 10px; */ 67 | /* height: 5px; */ 68 | /* } */ 69 | 70 | #root { 71 | height: inherit; 72 | margin: 0; 73 | background: #4b84aa; 74 | background-repeat: no-repeat; 75 | background-size: cover; 76 | } 77 | 78 | /* Tabs Container */ 79 | .Tabs { 80 | display: grid; 81 | grid-template-rows: 1fr 92.5%; 82 | overflow: auto; 83 | /* box-shadow: rgba(0, 0, 0, 0.3) 0px 19px 38px, 84 | rgba(0, 0, 0, 0.22) 0px 15px 12px; */ 85 | } 86 | 87 | .snap-inline { 88 | scroll-snap-type: inline mandatory; 89 | } 90 | 91 | .snap-inline > * { 92 | scroll-snap-align: start; 93 | } 94 | 95 | .task-container { 96 | height: fit-content; 97 | /* min-height: fit-content; */ 98 | padding-bottom: 1rem; 99 | position: relative; 100 | /* background-image: linear-gradient(to bottom, #121f6d, #131d5a); */ 101 | background: #19314d; 102 | z-index: 1; 103 | } 104 | 105 | .task-container::before { 106 | position: absolute; 107 | content: ''; 108 | top: 0; 109 | right: 0; 110 | bottom: 0; 111 | left: 0; 112 | height: 100%; 113 | border-radius: 0.375rem; 114 | background-image: linear-gradient(to bottom, #19314d, #355477, #19314d); 115 | z-index: -1; 116 | transition: opacity 1s linear; 117 | opacity: 0; 118 | } 119 | 120 | .task-container:hover::before { 121 | opacity: 1; 122 | } 123 | 124 | /* Remove browser defaults */ 125 | * { 126 | box-sizing: border-box; 127 | padding: 0; 128 | margin: 0; 129 | font-family: Georgia, 'Times New Roman', Times, sans-serif; 130 | } 131 | 132 | /* Style App.js wrapper */ 133 | /* .App { 134 | width: 100vw; 135 | height: 100vh; 136 | display: flex; 137 | align-items: center; 138 | justify-content: center; 139 | overflow: hidden; 140 | } */ 141 | 142 | /* .nav { 143 | margin-inline: auto; 144 | } */ 145 | 146 | /* Tab Navigation */ 147 | 148 | ul.nav li { 149 | /* font-size: medium; */ 150 | /* font-weight: 300; */ 151 | padding: 1.1rem; 152 | /* background-color: rgb(250, 250, 250); 153 | color: rgb(33, 33, 33); 154 | list-style: none; 155 | text-align: center; 156 | cursor: pointer; 157 | transition: all 0.5s; */ 158 | } 159 | 160 | ul.nav li:first-child { 161 | border-bottom-left-radius: 2rem; 162 | } 163 | 164 | ul.nav li:last-child { 165 | border-bottom-right-radius: 2rem; 166 | } 167 | 168 | ul.nav li:hover { 169 | box-shadow: 0px 0px 5px #f3f5f7; 170 | color: #f3f5f7; 171 | } 172 | 173 | ul.nav li.active { 174 | transition: all 0.7s; 175 | background: #19314d; 176 | color: rgb(219, 217, 217); 177 | } 178 | 179 | .container_component { 180 | height: fit-content; 181 | } 182 | /* Tab Content Styles */ 183 | .TabContent { 184 | /* display: grid; 185 | grid-auto-flow: row; 186 | grid-auto-columns: 40%; */ 187 | display: flex; 188 | flex-direction: column; 189 | font-size: 2rem; 190 | text-align: center; 191 | overflow-y: auto; 192 | /* overscroll-behavior-inline: contain; */ 193 | } 194 | 195 | @media only screen and (min-width: 1200) { 196 | .TabContent { 197 | background-color: red; 198 | } 199 | } 200 | 201 | /* ===== Scrollbar CSS ===== */ 202 | /* Firefox */ 203 | * { 204 | scrollbar-width: none; 205 | scrollbar-color: #919191 #ffffff; 206 | } 207 | 208 | /* Chrome, Edge, and Safari */ 209 | *::-webkit-scrollbar { 210 | width: 8px; 211 | /* display: none; */ 212 | } 213 | 214 | *::-webkit-scrollbar-track { 215 | background: inherit; 216 | } 217 | 218 | *::-webkit-scrollbar-thumb { 219 | background-color: #ececec; 220 | border-radius: 0.375rem; 221 | } 222 | 223 | #logo { 224 | font-size: 2rem; 225 | } 226 | 227 | .navigation { 228 | gap: 2rem; 229 | height: inherit; 230 | display: grid; 231 | grid-template-rows: 2fr 20fr 1fr; 232 | } 233 | 234 | .managerAndTabs { 235 | display: grid; 236 | grid-template-columns: 3fr 8fr; 237 | gap: 2rem; 238 | overflow: auto; 239 | } 240 | 241 | /* #tab3 { 242 | display: grid; 243 | grid-auto-flow: column; 244 | } */ 245 | 246 | /*LOADING WAVE ANIMATION*/ 247 | /* body { 248 | margin: 0; 249 | padding: 0; 250 | box-sizing: border-box; 251 | } */ 252 | .center { 253 | /* margin-left: 45%; */ 254 | width: 100%; 255 | height: 80%; 256 | display: flex; 257 | justify-content: center; 258 | align-items: center; 259 | /* margin-inline: auto; */ 260 | /* background: #000; */ 261 | } 262 | .wave { 263 | width: 25px; 264 | height: 400px; 265 | background: linear-gradient(45deg, #19314d, #fff); 266 | margin: 5px; 267 | animation: wave 1s linear infinite; 268 | border-radius: 20px; 269 | } 270 | .wave:nth-child(2) { 271 | animation-delay: 0.1s; 272 | } 273 | .wave:nth-child(3) { 274 | animation-delay: 0.2s; 275 | } 276 | .wave:nth-child(4) { 277 | animation-delay: 0.3s; 278 | } 279 | .wave:nth-child(5) { 280 | animation-delay: 0.4s; 281 | } 282 | .wave:nth-child(6) { 283 | animation-delay: 0.5s; 284 | } 285 | .wave:nth-child(7) { 286 | animation-delay: 0.6s; 287 | } 288 | .wave:nth-child(8) { 289 | animation-delay: 0.7s; 290 | } 291 | .wave:nth-child(9) { 292 | animation-delay: 0.8s; 293 | } 294 | .wave:nth-child(10) { 295 | animation-delay: 0.9s; 296 | } 297 | 298 | @keyframes wave { 299 | 0% { 300 | transform: scale(0); 301 | } 302 | 50% { 303 | transform: scale(1); 304 | } 305 | 100% { 306 | transform: scale(0); 307 | } 308 | } 309 | 310 | /* .lineChart { 311 | background-image: url('https://i.imgur.com/jW0PsjL.png'); 312 | background-size: cover; 313 | } */ 314 | .managerMetricsContainer { 315 | /* height: inherit; */ 316 | display: grid; 317 | grid-template-rows: 2fr 1fr 1fr; 318 | /* box-shadow: rgba(0, 0, 0, 0.3) 0px 19px 38px, */ 319 | /* rgba(0, 0, 0, 0.22) 0px 15px 12px; */ 320 | } 321 | 322 | .health-logs { 323 | display: grid; 324 | grid-template-rows: 1fr 4fr; 325 | } 326 | -------------------------------------------------------------------------------- /app/src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOMClient from 'react-dom/client'; 3 | import App from './components/App'; 4 | import { BrowserRouter } from 'react-router-dom'; 5 | import './index.css'; 6 | 7 | const root = ReactDOMClient.createRoot(document.getElementById('root')); 8 | root.render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orcastration", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production nodemon server/server.js", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "concurrently \"nodemon server/server.js\" \"cross-env NODE_ENV=development webpack serve --open\"", 10 | "test": "jest --verbose" 11 | }, 12 | "babel": { 13 | "presets": [ 14 | "@babel/preset-env", 15 | "@babel/preset-react" 16 | ] 17 | }, 18 | "nodemonConfig": { 19 | "ignore": [ 20 | "build", 21 | "client" 22 | ] 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/oslabs-beta/Orcastration.git" 27 | }, 28 | "keywords": [], 29 | "author": "Andrew Hogan, Max Heubel, Juliana Morrelli, Meimei Xiong, Danny Zheng", 30 | "license": "ISC", 31 | "bugs": { 32 | "url": "https://github.com/oslabs-beta/Orcastration/issues" 33 | }, 34 | "homepage": "https://github.com/oslabs-beta/Orcastration#readme", 35 | "devDependencies": { 36 | "@babel/core": "^7.20.7", 37 | "@babel/preset-env": "^7.20.2", 38 | "@babel/preset-react": "^7.18.6", 39 | "@svgr/webpack": "^6.5.1", 40 | "autoprefixer": "^10.4.13", 41 | "babel-loader": "^9.1.0", 42 | "css-loader": "^6.7.3", 43 | "dotenv-webpack": "^8.0.1", 44 | "file-loader": "^6.2.0", 45 | "html-webpack-plugin": "^5.5.0", 46 | "jest": "^29.4.0", 47 | "mini-css-extract-plugin": "^2.7.2", 48 | "node-sass": "^8.0.0", 49 | "nodemon": "^2.0.20", 50 | "postcss": "^8.4.20", 51 | "postcss-loader": "^7.0.2", 52 | "postcss-preset-env": "^7.8.3", 53 | "sass-loader": "^13.2.0", 54 | "style-loader": "^3.3.1", 55 | "supertest": "^6.3.3", 56 | "tailwindcss": "^3.2.4", 57 | "webpack": "^5.75.0", 58 | "webpack-cli": "^5.0.1", 59 | "webpack-dev-server": "^4.11.1" 60 | }, 61 | "dependencies": { 62 | "bcryptjs": "^2.4.3", 63 | "chart.js": "^3.9.1", 64 | "chartjs-adapter-luxon": "^1.3.0", 65 | "chartjs-plugin-streaming": "^2.0.0", 66 | "child_process": "^1.0.2", 67 | "concurrently": "^7.6.0", 68 | "config": "^3.3.9", 69 | "cors": "^2.8.5", 70 | "cross-env": "^7.0.3", 71 | "dotenv": "^16.0.3", 72 | "express": "^4.18.2", 73 | "framer-motion": "^8.1.7", 74 | "luxon": "^3.2.1", 75 | "mongoose": "^6.8.2", 76 | "node-fetch": "^3.3.0", 77 | "react": "^18.2.0", 78 | "react-chartjs-2": "^4.3.1", 79 | "react-dom": "^18.2.0", 80 | "react-router": "^6.6.1", 81 | "react-router-dom": "^6.6.1", 82 | "react-svg-loader": "^3.0.3", 83 | "util": "^0.12.5", 84 | "uuid": "^9.0.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | // module.exports = { 2 | // plugins: { 3 | // tailwindcss: {}, 4 | // autoprefixer: {}, 5 | // }, 6 | // } 7 | 8 | const tailwindcss = require('tailwindcss'); 9 | module.exports = { 10 | plugins: [ 11 | 'postcss-preset-env', 12 | tailwindcss 13 | ], 14 | }; -------------------------------------------------------------------------------- /server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | const cookieController = {}; 2 | 3 | cookieController.setSSIDCookie = (req, res, next) => { 4 | res.cookie('ssid', res.locals.user._id, { httpOnly: true }) 5 | return next(); 6 | } 7 | 8 | module.exports = cookieController; -------------------------------------------------------------------------------- /server/controllers/dockerContainerController.js: -------------------------------------------------------------------------------- 1 | const ContainerSnapshot = require('../models/containerSnapshotModel'); 2 | const uuid = require('uuid'); 3 | 4 | /* 5 | Import getNodeIDs, getRunningTaskIDs, and getTaskContainerIDs helper functions. See '../helperFunctions/dockerSwarmCLI.js' for more details. 6 | */ 7 | const { 8 | getNodeIDs, 9 | getRunningTaskIDs, 10 | getTaskContainerIDs, 11 | // getSwarmContainerInfo, 12 | } = require('../helperFunctions/dockerSwarmCLI.js'); 13 | 14 | /* 15 | Import getContainerStats helper function. See '../helperFunctions/dockerCLI.js' for more details. 16 | */ 17 | const { getContainerStats } = require('../helperFunctions/dockerCLI.js'); 18 | 19 | dockerContainerController = {}; 20 | 21 | /** 22 | * @description This middleware retrieves an object containing all tasks and containers running strictly on the first node in the Docker Swarm cluster. 23 | The information is used to populate the tasks and containers of the first node on the frontend upon landing/routing. 24 | The information on the tasks and containers of other nodes in the Docker Swarm cluster will be retreived by clicking seprate node tabs. See 'getStatsByNode' middleware. 25 | * @note This is done to modularize the code and reduce the amount of bandwidth and data being sent over the network through each HTTP request. 26 | * @param {Object} req - Express request object 27 | * @param {Object} res - Express response object 28 | * @param {function} next - Express next middleware function 29 | * @returns {function} next - Express next middleware function is returned after storing 'nodesData' in res.locals 30 | * @throws {Object} err - An object containing the error message and log. 31 | */ 32 | dockerContainerController.getTasksByNode = (req, res, next) => { 33 | // Get the list of nodes in the Docker Swarm cluster 34 | getNodeIDs() 35 | .then((nodeIDList) => { 36 | // Extract the first node in the list of node ID's 37 | const firstNodeID = [nodeIDList[0]]; 38 | return Promise.all( 39 | firstNodeID.map((nodeID) => { 40 | // Create an object for the current node 41 | const nodeData = { nodeID: nodeID, tasks: [] }; 42 | // Get the running tasks for the current node 43 | return getRunningTaskIDs(nodeID).then((runningTaskList) => { 44 | // Iterate over the running tasks 45 | return Promise.all( 46 | runningTaskList.map((taskID) => { 47 | // Create an object for the current task 48 | const taskData = { taskID: taskID, containers: [] }; 49 | // Get the container IDs for the current task 50 | return getTaskContainerIDs(taskID).then((containerIDList) => { 51 | // Populate the containers array with the list of container ID's 52 | taskData.containers = [...containerIDList]; 53 | return taskData; 54 | }); 55 | }) 56 | ).then((tasksData) => { 57 | // Populate the tasks array with the taskData objects containing the task ID and the array of container IDs associated with that task. 58 | nodeData.tasks = tasksData; 59 | return nodeData; 60 | }); 61 | }); 62 | }) 63 | ); 64 | }) 65 | .then((nodesData) => { 66 | // Store the promise that resolves to an object containing the node ID and an array of task objects for the first node in the Docker Swarm cluster. Each task object contains the task ID and an array of container IDs associated with that task. 67 | res.locals.dockerContainerStats = nodesData; 68 | return next(); 69 | }) 70 | .catch((err) => { 71 | return next({ 72 | log: `dockerContainerController.getTasksByNode: ERROR: ${err}`, 73 | message: { 74 | err: 'An error occurred in obtaining the tasks and containers of the first node in the Docker Swarm cluster.', 75 | }, 76 | }); 77 | }); 78 | }; 79 | 80 | /** 81 | * @description This middleware retrieves a list of container ID's from the request body, generates a new UUID, and sanitizes the container list. 82 | The sanitized container list and generated UUID are used as schema fields to create a new 'ContainerSnapshot' document in the database. 83 | This middleware is used to store a snapshot of the container IDs in the database for later use in the 'streamSwarmStats' middleware. 84 | * @note By separating this functionality into a separate middleware, it allows for better control and management of the data flow, partial rendering, 85 | as well as reducing load on the server. 86 | * @param {Object} req - Express request object 87 | * @param {Object} res - Express response object 88 | * @param {function} next - Express next middleware function 89 | * @return {function} next - Express next middleware function is returned after storing 'containerSnapshotUUID' in res.locals 90 | * @throws {Object} err - An object containing the error message and log. 91 | */ 92 | dockerContainerController.saveSwarmData = (req, res, next) => { 93 | const containerList = req.body.filter((id) => /^[A-Za-z0-9]*$/.test(id)); 94 | const UUID = uuid.v4(); 95 | ContainerSnapshot.create({ UUID, containerList }) 96 | .then(() => { 97 | res.locals.containerSnapshotUUID = UUID; 98 | return next(); 99 | }) 100 | .catch((err) => { 101 | return next({ 102 | log: `dockerContainterController.saveSwarmData: ERROR: ${err}`, 103 | message: { 104 | err: 'An error occurred in saving Docker Swarm cluster containers.', 105 | }, 106 | }); 107 | }); 108 | }; 109 | 110 | /** 111 | * @description This middleware streams the statistics of all containers in a Docker Swarm cluster using Server-Sent Events (SSE) technology. 112 | It utilizes the UUID provided in the request params to query the 'ContainerSnapshot' model and retrieve the list of container IDs. 113 | The list of container IDs is concatenated and passed to the 'getContainerStats' function which retrieves the statistics of all containers in one exec call, reducing load and bandwidth. 114 | A streaming interval of 1500ms is used to make real-time update to the statistics 115 | * @note This middleware is separated from the 'saveSwarmData' and 'getTasksByNode' middleware for better control and modularity of functionality, as well as for the purpose of reducing load on the server. 116 | * @param {Object} req - Express request object 117 | * @param {Object} res - Express response object 118 | * @param {function} next - Express next middleware function 119 | * @returns {void} 120 | */ 121 | dockerContainerController.streamSwarmStats = (req, res, next) => { 122 | // Set response headers for SSE compatibility 123 | res.writeHead(200, { 124 | 'Content-Type': 'text/event-stream', 125 | 'Cache-Control': 'no-cache', 126 | Connection: 'keep-alive', 127 | 'Access-Control-Allow-Origin': '*', 128 | }); 129 | const { UUID } = req.params; 130 | // Query the 'ContainerSnapshot' model in the database to find the list of containers associated to the unique UUID 131 | ContainerSnapshot.findOne({ UUID }) 132 | .then((containerListDoc) => { 133 | if (containerListDoc === null) { 134 | return next({ 135 | log: `dockerContainerController.streamSwarmStats: ContainerList with ${UUID} not found. ERROR: ${err}`, 136 | message: { 137 | err: 'An error occurred while attempting to find containers.', 138 | }, 139 | }); 140 | } 141 | // Concatenate the list of containers into one string to pass into 'getContainerStats' to retrieve the container stats in one exec call 142 | const concatenatedContainerIDs = containerListDoc.containerList.reduce( 143 | (acc, ID) => { 144 | return /^[A-Za-z0-9]*$/.test(ID) ? (acc += ID + ' ') : acc; 145 | }, 146 | '' 147 | ); 148 | return concatenatedContainerIDs; 149 | }) 150 | .then((concatenatedContainerIDs) => { 151 | // Set a streaming interval of 1500ms to retrieve real-time updates of the container statistics. 152 | const streamingInterval = setInterval(() => { 153 | getContainerStats(concatenatedContainerIDs) 154 | .then((containerStats) => { 155 | // The returned statistics are stringified and written to the response object with a 'data:' prefix, adhering to SSE conventions. 156 | const stringifiedContainerStats = JSON.stringify(containerStats); 157 | res.write(`data: ${stringifiedContainerStats}\n\n`); 158 | }) 159 | .catch((err) => { 160 | return next({ 161 | log: `dockerContainerController.streamSwarmStats: Error occured in 'streamSwarmStats' streamingInterval. ERROR: ${err}`, 162 | message: { 163 | err: 'An error occurred while streaming Docker Swarm cluster container stats', 164 | }, 165 | }); 166 | }); 167 | }, 1500); 168 | // Add 'close' event listener to clear the interval and end the response when the client closes the connection. 169 | res.on('close', () => { 170 | clearInterval(streamingInterval); 171 | res.end(); 172 | }); 173 | }) 174 | .catch((err) => { 175 | return next({ 176 | log: `dockerContainerController.streamSwarmStats: ERROR: ${err}`, 177 | message: { 178 | err: 'An error occurred while streaming Docker Swarm cluster container stats.', 179 | }, 180 | }); 181 | }); 182 | }; 183 | 184 | // Unused by FE 185 | // dockerContainerController.getContainers = (req, res, next) => { 186 | // getSwarmContainerInfo().then((swarmContainerList) => { 187 | // console.log(swarmContainerList); 188 | // const containerStatus = swarmContainerList.map((container) => { 189 | // return { 190 | // createdAt: container.CreatedAt, 191 | // containerID: container.ID, 192 | // containerName: container.Names, 193 | // image: container.Image, 194 | // size: container.Size, 195 | // state: container.State, 196 | // containerStatus: container.Status, 197 | // }; 198 | // }); 199 | // res.locals.dockerContData = containerStatus; 200 | // return next().catch((err) => { 201 | // return next({ 202 | // log: `dockerContainterController.getStatus: ERROR: ${err}`, 203 | // message: { err: "An error occurred in obtaining container status'." }, 204 | // }); 205 | // }); 206 | // }); 207 | // }; 208 | 209 | module.exports = dockerContainerController; 210 | -------------------------------------------------------------------------------- /server/controllers/dockerSwarmController.js: -------------------------------------------------------------------------------- 1 | const { getContainerHealth } = require('../helperFunctions/dockerCLI.js'); 2 | 3 | dockerSwarmController = {}; 4 | 5 | /** 6 | * @description This middleware retrieves the health status and logs of a container 7 | * The information is used to allow users to monitor the health status and logs of a container with the click of a button 8 | * @param {*} req - Express request object 9 | * @param {*} res - Express response object 10 | * @param {*} next - Express next middleware function 11 | * @returns {function} next - Express next middleware function is returned after storing 'healthData' in res.locals 12 | */ 13 | dockerSwarmController.getHealth = (req, res, next) => { 14 | const containerID = req.params.containerID; 15 | getContainerHealth(containerID) 16 | .then((healthData) => { 17 | res.locals.healthData = healthData; 18 | console.log(res.locals.healthData); 19 | return next(); 20 | }) 21 | .catch((err) => { 22 | return next({ 23 | log: `dockerSwarmController.getHealth: ERROR: ${err}`, 24 | message: { 25 | err: 'An error occurred in obtaining container health status.', 26 | }, 27 | }); 28 | }); 29 | }; 30 | 31 | module.exports = dockerSwarmController; 32 | -------------------------------------------------------------------------------- /server/controllers/sessionController.js: -------------------------------------------------------------------------------- 1 | const Session = require('../models/sessionModel') 2 | 3 | const sessionController = {}; 4 | 5 | sessionController.isLoggedIn = (req, res, next) => { 6 | const { ssid } = req.cookies 7 | Session.find({cookieId: ssid}) 8 | .then((data) => { 9 | if(data.length === 0){ 10 | res.redirect('/signup') 11 | } 12 | if(data){ 13 | return next() 14 | } 15 | }) 16 | .catch((err) => { 17 | return next({err: 'Error in isLoggedIn'}) 18 | }) 19 | } 20 | 21 | sessionController.startSession = (req, res, next) => { 22 | const { _id } = res.locals.user 23 | Session.create({cookieId: _id}) 24 | .then((data) => { 25 | if(data){ 26 | return next() 27 | } 28 | }) 29 | .catch((err) => { 30 | return next({err: 'Error in startSession'}) 31 | }) 32 | } 33 | 34 | module.exports = sessionController; -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const User = require('../models/userModel'); 3 | const bcrypt = require('bcryptjs'); 4 | 5 | const userController = {}; 6 | 7 | userController.createUser = (req, res, next) => { 8 | const { email, password } = req.body; 9 | 10 | User.create({ email, password }) 11 | .then((userDoc) => { 12 | res.locals.user = userDoc; 13 | return next(); 14 | }) 15 | .catch((err) => { 16 | if (err.name === 'MongoServerError' && err.code === 11000) { 17 | return next({ 18 | log: 'userController.createUser', 19 | status: 400, 20 | message: { err: 'The email has already been taken.' }, 21 | }); 22 | } 23 | next({ 24 | log: `userController.createUser: ERROR: ${err}`, 25 | message: { err: 'An error occurred in creating new user.' }, 26 | }); 27 | }); 28 | }; 29 | 30 | userController.verifyUser = (req, res, next) => { 31 | const { email, password } = req.body; 32 | User.findOne({ email }) 33 | .then((userDoc) => 34 | bcrypt.compare(password, userDoc.password).then((match) => { 35 | if (match) { 36 | res.locals.user = userDoc; 37 | return next(); 38 | } else { 39 | return next({ 40 | log: 'userController.verifyUser: Error: email or password is incorrect.', 41 | status: 400, 42 | message: { err: 'email or password is incorrect.' }, 43 | }); 44 | } 45 | }) 46 | ) 47 | .catch((err) => { 48 | return next({ 49 | log: `userController.verifyUser: Error: ${err}`, 50 | message: { err: 'An error occured in verifying user.' }, 51 | }); 52 | }); 53 | }; 54 | 55 | userController.getUser = (req, res, next) => { 56 | 57 | // console.log('req.params:', req.params); 58 | const checkUser = req.body.email; 59 | // console.log(checkUser); 60 | User.findOne({ email: checkUser }) 61 | .then((user) => { 62 | if (user) { 63 | res.locals.user = user; 64 | } 65 | return next(); 66 | }) 67 | .catch((err) => { 68 | console.log('User is not found'); 69 | return next({ message: 'An error occurred in getUser' }); 70 | }); 71 | }; 72 | 73 | module.exports = userController; 74 | -------------------------------------------------------------------------------- /server/helperFunctions/dockerCLI.js: -------------------------------------------------------------------------------- 1 | /* 2 | Import execProm helper function. See '../helperFunctions/execProm.js' for more details. 3 | */ 4 | const { execProm } = require('../helperFunctions/execProm.js'); 5 | 6 | /* 7 | Import parseRawData and parseRawDataIntoObject helper functions. See '../helperFunctions/parsers.js' for more details. 8 | */ 9 | const { 10 | parseRawData, 11 | parseRawDataIntoObject, 12 | } = require('../helperFunctions/parsers.js'); 13 | 14 | /** 15 | * @description Retrieve non-streamed Docker container stats for one or more containers 16 | * 17 | * @param {string} containerIDs - The container IDs to retrieve stats for. 18 | * @returns {Promise} - A promise that resolves to an object containing the parsed container stats. 19 | */ 20 | const getContainerStats = (containerIDs) => { 21 | return execProm( 22 | `docker stats ${containerIDs} --no-stream --format "{{json .}}"` 23 | ).then((rawContainerStats) => { 24 | const parsedContainerStats = parseRawDataIntoObject(rawContainerStats); 25 | return parsedContainerStats; 26 | }); 27 | }; 28 | 29 | // getContainerStats( 30 | // 'f8340e7c21398988aa145cb3437b70fd895944e910b6b8ab624548e1afe09997' + 31 | // ' ' + 32 | // '20975d85d546b88ae9c1676268fef9aaefb2597120d18f9685865e8210e9781d' 33 | // ).then((containerStats) => { 34 | // console.log(containerStats); 35 | // }); 36 | 37 | /** 38 | * @description Retrieve Docker container info for one container 39 | * 40 | * @param {string} containerID - The container ID to retrieve info for. 41 | * @returns {Promise} - A promise that resolves to an array containing the parsed container info. 42 | */ 43 | const getContainerInfo = (containerID) => { 44 | return execProm( 45 | `docker ps --filter "id=${containerID}" --format "{{json .}}"` 46 | ).then((rawContainerData) => { 47 | const parsedContainerData = parseRawData(rawContainerData); 48 | return parsedContainerData; 49 | }); 50 | }; 51 | 52 | // getContainerInfo( 53 | // 'f8340e7c21398988aa145cb3437b70fd895944e910b6b8ab624548e1afe09997' 54 | // ).then((containerStats) => { 55 | // console.log(containerStats); 56 | // }); 57 | 58 | /** 59 | * @description Retrieve Docker health status and logs for one container 60 | * 61 | * @param {string} containerID - The container ID to retrieve health status and logs for. 62 | * @returns {Promise} - A promise that resolves to an array containing the parsed container health status and logs. 63 | */ 64 | const getContainerHealth = (containerID) => { 65 | return execProm( 66 | `docker inspect ${containerID} --format="{{json .State.Health}}"` 67 | ).then((rawHealthData) => { 68 | const parsedRawHealthData = parseRawData(rawHealthData); 69 | return parsedRawHealthData; 70 | }); 71 | }; 72 | 73 | // getContainerHealth( 74 | // 'ea07a765d32457ff5fc95b009562a3d939f59533dab6ee4b5b915d6febd66e32' 75 | // ).then((containerHealth) => { 76 | // console.log(containerHealth); 77 | // }); 78 | 79 | module.exports = { 80 | getContainerStats, 81 | getContainerInfo, 82 | getContainerHealth, 83 | }; 84 | -------------------------------------------------------------------------------- /server/helperFunctions/dockerSwarmCLI.js: -------------------------------------------------------------------------------- 1 | /* 2 | Import execProm helper function. See '../helperFunctions/execProm.js' for more details. 3 | */ 4 | const { execProm } = require('../helperFunctions/execProm.js'); 5 | 6 | /* 7 | Import parseRawData and parseRawDataIntoObject helper functions. See '../helperFunctions/parsers.js' for more details. 8 | */ 9 | const { parseRawData } = require('../helperFunctions/parsers.js'); 10 | 11 | /** 12 | * @description Retrieve Docker Swarm node ID's 13 | * @param {none} none - No input parameters 14 | * @returns {Promise} - A promise that resolves to an array containing the parsed node IDs 15 | */ 16 | const getNodeIDs = () => { 17 | return execProm('docker node ls --format "{{json .ID}}"').then( 18 | (rawNodeIDs) => { 19 | const parsedNodeIDs = parseRawData(rawNodeIDs); 20 | return parsedNodeIDs; 21 | } 22 | ); 23 | }; 24 | 25 | // getNodeIDs().then((nodeIDs) => { 26 | // console.log(nodeIDs); 27 | // }); 28 | 29 | /** 30 | * @description Retrieve Docker Swarm 'running' task ID's of one node 31 | * 32 | * @param {string} nodeID - The node ID to retrieve the task IDs from 33 | * @returns {Promise} - A promise that resolves to an array containing the parsed running task IDs 34 | */ 35 | const getRunningTaskIDs = (nodeID) => { 36 | return execProm( 37 | `docker node ps ${nodeID} --filter desired-state=running --format "{{json .ID}}"` 38 | ).then((rawTaskIDs) => { 39 | const parsedRunningTaskIDs = parseRawData(rawTaskIDs); 40 | return parsedRunningTaskIDs; 41 | }); 42 | }; 43 | 44 | // getRunningTaskIDs('01hce8ymcxnkc10hhsgqusb0t').then((taskIDs) => { 45 | // console.log(taskIDs); 46 | // }); 47 | 48 | /** 49 | * @description Retrieve Docker Swarm 'shutdown' task ID's 50 | * 51 | * @param {string} nodeID - The node ID to retrieve the task IDs from 52 | * @returns {Promise} - A promise that resolves to an array containing the parsed shutdown task IDs 53 | */ 54 | const getShutdownTaskIDs = (nodeID) => { 55 | return execProm( 56 | `docker node ps ${nodeID} --filter desired-state=shutdown --format "{{json .ID}}"` 57 | ).then((rawTaskIDs) => { 58 | const parsedShutdownTaskIDs = parseRawData(rawTaskIDs); 59 | return parsedShutdownTaskIDs; 60 | }); 61 | }; 62 | 63 | // getShutdownTaskIDs('01hce8ymcxnkc10hhsgqusb0t').then((taskIDs) => { 64 | // console.log(taskIDs); 65 | // }); 66 | 67 | /** 68 | * @description Retrieve Docker Swarm container ID's filtered by task ID 69 | * 70 | * @param {string} taskID - The task ID to retrieve the container IDs from 71 | * @returns {Promise} - A promise that resolves to an array containing the parsed container IDs 72 | */ 73 | const getTaskContainerIDs = (taskID) => { 74 | return execProm( 75 | `docker inspect ${taskID} --format='{{json .Status.ContainerStatus.ContainerID}}'` 76 | ).then((rawContainerIDs) => { 77 | const parsedContainerIDs = parseRawData(rawContainerIDs); 78 | return parsedContainerIDs; 79 | }); 80 | }; 81 | 82 | // getTaskContainerIDs('d57ntl1o16jk').then((containerIDs) => { 83 | // console.log(containerIDs); 84 | // }); 85 | 86 | /** 87 | * @description Retrieve info for all containers in Docker Swarm 88 | * 89 | * @param {none} none - No input parameters 90 | * @returns {Promise} - A promise that resolves to an array containing the parsed container info of all the containers in the swarm 91 | */ 92 | const getSwarmContainerInfo = () => { 93 | return execProm( 94 | // only list the containers in the swarm 95 | 'docker ps --all --format "{{json .}}" --filter "label=com.docker.swarm.service.name"' 96 | ).then((rawSwarmContainerData) => { 97 | const parsedSwarmContainerData = parseRawData(rawSwarmContainerData); 98 | return parsedSwarmContainerData; 99 | }); 100 | }; 101 | 102 | // getSwarmContainerInfo().then((swarmContainerData) => { 103 | // console.log(swarmContainerData); 104 | // }); 105 | 106 | module.exports = { 107 | getNodeIDs, 108 | getRunningTaskIDs, 109 | getShutdownTaskIDs, 110 | getTaskContainerIDs, 111 | getSwarmContainerInfo, 112 | }; 113 | -------------------------------------------------------------------------------- /server/helperFunctions/execProm.js: -------------------------------------------------------------------------------- 1 | /* 2 | Import promisify function from 'util' library and exec function from 'child_process" library 3 | */ 4 | const { promisify } = require('util'); 5 | const { exec } = require('child_process'); 6 | 7 | /** 8 | * @description Convert the callback-based exec function to a promise-based function for cleaner syntax and better error handling 9 | * 10 | * @param {string} command - The command to be executed 11 | * @returns {Promise<{stdout: string, stderr: string}>} A promise that resolves to an object containing the stdout and stderr of the command execution 12 | */ 13 | const execProm = promisify(exec); 14 | 15 | module.exports = {execProm} -------------------------------------------------------------------------------- /server/helperFunctions/parsers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Parses the stdout from executing CLI through child_process and returns an array 3 | * 4 | * @param {Object} rawData - The object containing stdout from executing the command 5 | * @returns {Array} - An array containing the parsed data from the stdout 6 | */ 7 | const parseRawData = (rawData) => { 8 | const stdout = rawData.stdout.trim().split('\n'); 9 | const parsedData = stdout.map((rawData) => JSON.parse(rawData)); 10 | return parsedData; 11 | }; 12 | 13 | /** 14 | * @description Parses the stdout from executing CLI through child_process and returns an object 15 | * 16 | * @param {Object} rawData - The object containing stdout from executing the command 17 | * @returns {Object} - An object containing container ID's as the key and the parsed data as the value 18 | */ 19 | const parseRawDataIntoObject = (rawData) => { 20 | const parsedDataObject = {}; 21 | const stdout = rawData.stdout.trim().split('\n'); 22 | stdout.forEach((rawData) => { 23 | const parsedData = JSON.parse(rawData); 24 | const containerID = parsedData.Container; 25 | parsedDataObject[containerID] = parsedData; 26 | }); 27 | return parsedDataObject; 28 | }; 29 | 30 | module.exports = { parseRawData, parseRawDataIntoObject }; 31 | -------------------------------------------------------------------------------- /server/models/containerSnapshotModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | /* 5 | Create Mongoose schema for 'ContainerSnapshot' model 6 | Schema will have a UUID and containerList field that stores a snapshot of the list of containers active in the user's Docker Swarm 7 | The universally unique identifier (UUID) will be used in an event source to retrieve data 8 | */ 9 | const containerSnapshotSchema = new Schema( 10 | { 11 | UUID: { type: String, unique: true }, 12 | containerList: [], 13 | }, 14 | { minimize: false } 15 | ); 16 | 17 | module.exports = mongoose.model('ContainerSnapshot', containerSnapshotSchema); 18 | -------------------------------------------------------------------------------- /server/models/sessionModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | /* 5 | Create Mongoose schema for 'Session' model 6 | Schema will have a cookieID and createdAt field that will be used to handle a session for a logged in user 7 | */ 8 | const sessionSchema = new Schema({ 9 | cookieId: { type: String, required: true, unique: true }, 10 | createdAt: { type: Date, expires: 10, default: Date.now } 11 | }); 12 | 13 | module.exports = mongoose.model('Session', sessionSchema); -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | const bcrypt = require('bcryptjs'); 4 | 5 | /* 6 | Create Mongoose schema for 'User' model 7 | Schema will have a email and password field that allows users to create an account 8 | */ 9 | const userSchema = new Schema({ 10 | email: { type: String, required: true, unique: true }, 11 | password: { type: String, required: true }, 12 | }); 13 | 14 | /* 15 | Encypt the password in userSchema with Bcrypt hashing prior to saving it to the database 16 | */ 17 | userSchema.pre('save', function (next) { 18 | const user = this; 19 | bcrypt 20 | .genSalt(10) 21 | .then((salt) => bcrypt.hash(user.password, salt)) 22 | .then((hash) => { 23 | user.password = hash; 24 | return next(); 25 | }) 26 | .catch((err) => { 27 | return next({ 28 | log: 'Error in hashing of user password:' + JSON.stringify(err), 29 | message: { err: 'An error occured in creating user password.' }, 30 | }); 31 | }); 32 | }); 33 | 34 | module.exports = mongoose.model('User', userSchema); 35 | -------------------------------------------------------------------------------- /server/routes/dockerContainerRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const dockerContainerController = require('../controllers/dockerContainerController'); 4 | 5 | 6 | // unused 7 | // router.get( 8 | // '/getContainers', 9 | // dockerContainerController.getContainers, 10 | // (req, res) => { 11 | // return res.status(200).json(res.locals.dockerContData); 12 | // } 13 | // ); 14 | 15 | router.get( 16 | '/getTasks', 17 | dockerContainerController.getTasksByNode, 18 | (req, res) => { 19 | return res.status(200).json(res.locals.dockerContainerStats); 20 | } 21 | ); 22 | 23 | router.post( 24 | '/saveSwarmData', 25 | dockerContainerController.saveSwarmData, 26 | (req, res) => { 27 | return res.status(200).json(res.locals.containerSnapshotUUID); 28 | } 29 | ); 30 | 31 | router.get( 32 | '/streamSwarmStats/:UUID', 33 | dockerContainerController.streamSwarmStats, 34 | ); 35 | 36 | module.exports = router; 37 | -------------------------------------------------------------------------------- /server/routes/dockerSwarmRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const dockerSwarmController = require('../controllers/dockerSwarmController'); 4 | 5 | router.get('/getHealth/:containerID', dockerSwarmController.getHealth, (req, res) => { 6 | return res.status(200).json(res.locals.healthData) 7 | }) 8 | 9 | // unused 10 | // router.get('/getNodes', dockerSwarmController.getNodes, (req, res) => { 11 | // return res.status(200).json(res.locals.swarmNodeData); 12 | // }); 13 | 14 | // unused 15 | // router.get('/getTasks/:nodeID', dockerSwarmController.getTasks, (req, res) => { 16 | // // need to send the tasks of specified node through res.locals 17 | // return res.status(200).send(req.params); 18 | // }); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | // const cookieParser = require('cookie-parser'); 4 | const userController = require('../controllers/userController'); 5 | const cookieController = require('../controllers/cookieController'); 6 | const sessionController = require('../controllers/sessionController'); 7 | 8 | // router.get('/:_email', userController.getUser, (req, res) => { 9 | // return res.status(200).send(res.locals.checkUser); 10 | // }); 11 | 12 | // cookieController.setSSIDCookie, sessionController.startSession, 13 | 14 | router.post('/signup', userController.createUser, (req, res) => { 15 | return res.status(200).json(res.locals.user); 16 | }); 17 | 18 | // cookieController.setSSIDCookie, sessionController.startSession, 19 | 20 | router.post('/login', userController.verifyUser, (req, res) => { 21 | return res.status(200).json(res.locals.user); 22 | }); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | const PORT = 3000; 5 | const mongoose = require('mongoose'); 6 | const dotenv = require('dotenv').config({ path: './.env' }); 7 | 8 | /* 9 | Declare MongoDB Atlas URI to connect to MongoDB server 10 | */ 11 | const MONGO_URI = process.env.MONGO_URI; 12 | 13 | /* 14 | Connect to MongoDB databse 15 | */ 16 | mongoose 17 | .connect(MONGO_URI, { 18 | useNewUrlParser: true, 19 | useUnifiedTopology: true, 20 | dbName: 'OrcastrationDB', 21 | }) 22 | .then(() => console.log('Connected to Mongo DB.')) 23 | .catch((err) => console.log(err)); 24 | 25 | /* 26 | Route Handlers: 27 | */ 28 | const dockerContainerRouter = require('./routes/dockerContainerRouter'); 29 | const dockerSwarmRouter = require('./routes/dockerSwarmRouter'); 30 | const userRouter = require('./routes/user'); 31 | 32 | /* 33 | Set headers for configuring the browser's same-origin policy and handling CORS for the application 34 | */ 35 | app.use((req, res, next) => { 36 | res.setHeader('Access-Control-Allow-Origin', '*'); 37 | res.setHeader('Access-Control-Allow-Credentials', 'true'); 38 | res.setHeader('Access-Control-Allow-Methods', 'GET,HEAD,OPTIONS,POST,PUT'); 39 | res.setHeader( 40 | 'Access-Control-Allow-Headers', 41 | 'Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers' 42 | ); 43 | res.status(200); 44 | next(); 45 | }); 46 | 47 | /* 48 | Add middleware to parse incoming requsts in JSON format and URL encoded data. 49 | */ 50 | app.use(express.json()); 51 | app.use(express.urlencoded({ extended: true })); 52 | 53 | /* 54 | Handle requests for static files 55 | */ 56 | app.use(express.static(path.resolve(__dirname, '../app'))); 57 | 58 | /* 59 | Mount imported route handlers to specific routes 60 | */ 61 | app.use('/user', userRouter); 62 | app.use('/dockerCont', dockerContainerRouter); 63 | app.use('/dockerSwarm', dockerSwarmRouter); 64 | 65 | /* 66 | 404 error handler 67 | */ 68 | app.get('*', (req, res) => { 69 | return res.status(400).send('This page does not exist. Try again!'); 70 | }); 71 | 72 | /* 73 | Global error handler 74 | */ 75 | app.use((err, req, res, next) => { 76 | if (res.headersSent) { 77 | return next(err); 78 | } 79 | const defaultErr = { 80 | log: 'Express error handler caught unknown middleware error', 81 | status: 500, 82 | message: { err: 'An error occurred' }, 83 | }; 84 | const errorObj = Object.assign({}, defaultErr, err); 85 | return res.status(errorObj.status).json(errorObj.message); 86 | }); 87 | 88 | /* 89 | Start the server to listen for incoming HTTP requests on specified PORT 90 | */ 91 | app.listen(PORT, () => { 92 | console.log('Server listening on port 3000'); 93 | }); 94 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./app/src/**/*.{js,jsx}'], 4 | theme: { 5 | extend: { 6 | colors: { 7 | nightblue: { 8 | 100: '#1A3AC7', 9 | 200: '#1D37AE', 10 | 300: '#19314D', //darker blue 11 | 400: '#131F6E', 12 | 500: '#182D87', 13 | 600: '#131D5A', 14 | 700: '#0A1032', 15 | 800: '#050824', 16 | }, 17 | darkblue: { 18 | 500: '#19314D', 19 | }, 20 | lightgrey: '#F3F5F7', //the overall background color, not accessed via tailwind but set in index.css 21 | midblue: '#4B84AA', 22 | secondarymidblue: '#6789b0', 23 | lightblue: '#95C6EF', 24 | grey: '#525251', 25 | 26 | lavender: { 27 | 200: '#DFDDE7', 28 | 300: '#C9C5D8', 29 | }, 30 | pingloader: '#4bc0c0bf', 31 | custompurple: '#b517d4', 32 | bubbleblue: '#54cbf5', 33 | }, 34 | }, 35 | }, 36 | plugins: [], 37 | }; 38 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const Dotenv = require('dotenv-webpack'); 4 | 5 | module.exports = { 6 | mode: process.env.Node_ENV, 7 | entry: './app/src/index.js', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'index_bundle.js', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.svg$/, 16 | use: ['@svgr/webpack'], 17 | }, 18 | { 19 | test: /\.jsx?/, 20 | exclude: /node_modules/, 21 | loader: 'babel-loader', 22 | }, 23 | { 24 | test: /\.css$/i, 25 | include: path.resolve(__dirname, './app/src'), 26 | use: ['style-loader', 'css-loader', 'postcss-loader'], 27 | }, 28 | { 29 | test: /\.(png|jpg|gif)$/i, 30 | use: [ 31 | { 32 | loader: 'file-loader', 33 | options: { 34 | name: '[path][name].[ext]', 35 | }, 36 | }, 37 | ], 38 | }, 39 | ], 40 | }, 41 | plugins: [ 42 | new HtmlWebpackPlugin({ 43 | template: './app/public/index.html', 44 | filename: 'index.html', 45 | }), 46 | new Dotenv({ 47 | path: './.env', 48 | safe: true, 49 | allowEmptyValues: true, 50 | systemvars: true, 51 | silent: true, 52 | defaults: false, 53 | prefix: 'process.env', 54 | }), 55 | ], 56 | devServer: { 57 | static: { 58 | directory: path.resolve(__dirname, './app'), 59 | }, 60 | proxy: { 61 | '/': { 62 | target: 'http://localhost:3000', 63 | }, 64 | }, 65 | compress: true, 66 | port: 8080, 67 | }, 68 | resolve: { 69 | extensions: ['.jsx', '...'], 70 | }, 71 | }; 72 | --------------------------------------------------------------------------------