├── .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 |
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 |
8 | Orcastration
9 |
10 |
15 | create account
16 |
17 |
18 |
19 |
Log In
20 |
21 |
email:
22 |
23 |
password:
24 |
25 |
26 |
30 | {' '}
31 | enter{' '}
32 |
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 |
8 | Orcastration
9 |
10 |
15 | sign in
16 |
17 |
18 |
19 |
Create User
20 |
21 |
email:
22 |
23 |
password:
24 |
25 |
26 |
30 | {' '}
31 | enter{' '}
32 |
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 |
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 |
61 | Health Check
62 |
63 |
64 | {containerData && (
65 |
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 |
18 | Start Streaming
19 |
20 | ),
21 | Start: (
22 |
23 | Streaming Docker Swarm Container Metrics
24 |
25 |
26 |
27 |
28 | setCurrentStep('Stop')}
30 | className='bg-red-400 mt-2 shadow-md rounded-md p-2 text-lg text-slate-100 transition ease-in-out duration-300 hover:bg-red-700'
31 | >
32 | Pause Stream
33 |
34 |
35 | ),
36 | Stop: (
37 |
41 | Continue Streaming
42 |
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 |
9 | Orcastration
10 |
11 | {/*
16 | Sign Out
17 | */}
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 |
--------------------------------------------------------------------------------