├── .gitignore
├── LICENSE
├── README.md
├── __tests__
├── dbHelpertests.js
├── dockerCliHelperTests.js
├── reactTests.js
└── supertest.js
├── client
├── App.jsx
├── ProtectedRoute.jsx
├── assets
│ ├── delete.png
│ ├── logo.gif
│ ├── logoNoText.png
│ ├── logoTextOnly.png
│ ├── restart.png
│ ├── stop.png
│ ├── whaleBlue.png
│ └── whaleRed.png
├── components
│ ├── NavBar
│ │ └── NavBar.jsx
│ ├── authentication
│ │ ├── Login.jsx
│ │ ├── SignUpForm.js
│ │ ├── form.js
│ │ └── validation.js
│ ├── dashboard
│ │ ├── AverageCPUChart.jsx
│ │ ├── AverageMemoryChart.jsx
│ │ ├── BlockIOChart.jsx
│ │ ├── NetIOChart.jsx
│ │ ├── PIDChart.jsx
│ │ ├── Whale.jsx
│ │ └── WhaleChart.jsx
│ ├── dockerContainer
│ │ ├── DndContainers.jsx
│ │ ├── EachContainer.jsx
│ │ ├── ItemTypes.jsx
│ │ ├── Restart.jsx
│ │ └── Stop.jsx
│ └── notification
│ │ └── Notification.jsx
├── containers
│ ├── ContainersContainer.jsx
│ ├── DashboardContainer.jsx
│ ├── NotificationsContainer.jsx
│ ├── SettingsContainer.jsx
│ └── containerHelpers.js
├── index.html
├── index.js
├── scss
│ ├── Application.scss
│ └── ContainersContainer.scss
└── styles.scss
├── main.js
├── package-lock.json
├── package.json
├── server
├── db
│ ├── connect.js
│ ├── schema.js
│ └── setup.sql
├── helpers
│ ├── dbHelper.js
│ ├── dockerApiHelper.js
│ └── dockerCliHelper.js
└── index.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | node_modules
3 | build
4 | npm-debug.log
5 | .DS_Store
6 | .env
7 | .env.test
8 | out/
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WhaleWatch
2 |
3 |
4 |
5 |
6 | Table of Contents
7 |
14 |
15 | What is WhaleWatch?
16 | WhaleWatch is a lightweight, open-source monitoring tool for Docker. WhaleWatch enables developers to monitor Docker containers through a dashboard that delivers real-time metrics backed by intuitive data visualizations. Developers will be able to keep an eye on the containers that are critical for smooth technical and business operations and also proactively take action before troubles arise.
17 |
18 | WhaleWatch Features
19 |
20 |
21 | The ability to collect and display Docker metrics
22 | Monitor containers and identify trends through data visualizations
23 | Connect to Docker Daemon
24 | Drag and drop functionality to stop, start, and restart containers
25 |
26 |
27 | Installation and Setup
28 | WhaleWatch can be installed and set up with the following steps:
29 |
30 | 1. Fork and clone the repo
31 | 2. Run `npm install`
32 | 3. Create a `.env` file in your top level folder and add a Postgres URI as a variable labeled `DB_URI`
33 | 4. Run the following command in your Postgres instance:
34 | ```
35 | CREATE TABLE IF NOT EXISTS users (
36 | id SERIAL PRIMARY KEY,
37 | username varchar(50) UNIQUE NOT NULL,
38 | email varchar(100) UNIQUE NOT NULL,
39 | password varchar(100) NOT NULL
40 | );
41 |
42 | CREATE TABLE IF NOT EXISTS containers (
43 | id SERIAL PRIMARY KEY,
44 | dockerId varchar(100) NOT NULL,
45 | name varchar(100) NOT NULL,
46 | size varchar(50),
47 | status varchar(50),
48 | state varchar(50),
49 | owner integer REFERENCES users(id) NOT NULL
50 | );
51 |
52 | CREATE TABLE IF NOT EXISTS stats (
53 | id SERIAL PRIMARY KEY,
54 | container integer REFERENCES containers(id) NOT NULL,
55 | timestamp date NOT NULL,
56 | cpuUsage decimal NOT NULL,
57 | memUsage decimal NOT NULL,
58 | netIo varchar(50) NOT NULL,
59 | blockIo varchar(50) NOT NULL,
60 | pids integer NOT NULL,
61 | reqPerMin integer
62 | )
63 | ```
64 | 5. Ensure Docker Daemon is running on your computer.
65 | 6. Run `npm run dev`
66 |
67 | Navigating and Using WhaleWatch
68 | WhaleWatch is a tool that developers will be able to utilize through their browsers as a web application.
69 |
70 | 
71 |
72 | Upon launching the application, the user will be asked to sign up or provide login credentials. Once the sign-up or login step has been completed, the user will be redirected to the main WhaleWatch dashboard. This dashboard will contain key data and metrics such as:
73 |
74 |
75 | Average CPU Usage
76 | Average Memory Usage
77 | Average Net I/O
78 | Average Block I/O
79 | Average PIDs
80 |
81 |
82 | 
83 |
84 | Furthermore, in the Container Health Overview section, developers will be able to quickly glean insights into which containers are healthy (blue whales) and which containers require attention (red whales).
85 |
86 | Within the Containers component, developers have the ability to easily start, stop and restart containers with ease through intuitive drag and drop functionality. The containers are displayed and organized categorically by “Active” and “Inactive” containers.
87 |
88 | Meet the Team
89 |
95 |
--------------------------------------------------------------------------------
/__tests__/dbHelpertests.js:
--------------------------------------------------------------------------------
1 | const dbHelper = require('../server/helpers/dbHelper');
2 |
3 | describe('Database helper unit tests', () => {
4 | describe('refresh container data', () => {
5 | it('')
6 | })
7 | describe('refresh stats data', () => {
8 |
9 | })
10 | })
--------------------------------------------------------------------------------
/__tests__/dockerCliHelperTests.js:
--------------------------------------------------------------------------------
1 | const { dockerCliHelper, parseCliJSON } = require('../server/helpers/dockerCliHelper');
2 |
3 | describe('Docker CLI Helper Unit Tests', () => {
4 |
5 | describe('parseCliJSON', () => {
6 | const fakeData = `{"CreatedAt":"2021-08-27 11:14:06 -0400 EDT","ID":"5e92d0ef966e","Image":"bobrik/socat","Labels":"desktop.docker.io/binds/0/Source=/var/run/docker.sock,desktop.docker.io/binds/0/SourceKind=dockerSocketProxied,desktop.docker.io/binds/0/Target=/var/run/docker.sock","LocalVolumes":"0","Mounts":"/run/host-serv…","Names":"socat","Networks":"bridge","Ports":"127.0.0.1:2375-\u003e2375/tcp","RunningFor":"8 days ago","Size":"0B (virtual 6.95MB)","State":"running","Status":"Up 23 hours"}`;
7 | const fakeMultilineData = `{"CreatedAt":"2021-08-27 11:14:06 -0400 EDT","ID":"5e92d0ef966e","Image":"bobrik/socat","Labels":"desktop.docker.io/binds/0/Source=/var/run/docker.sock,desktop.docker.io/binds/0/SourceKind=dockerSocketProxied,desktop.docker.io/binds/0/Target=/var/run/docker.sock","LocalVolumes":"0","Mounts":"/run/host-serv…","Names":"socat","Networks":"bridge","Ports":"127.0.0.1:2375-\u003e2375/tcp","RunningFor":"8 days ago","Size":"0B (virtual 6.95MB)","State":"running","Status":"Up 23 hours"}
8 | {"CreatedAt":"2021-08-27 11:14:06 -0400 EDT","ID":"5e92d0ef966e","Image":"bobrik/socat","Labels":"desktop.docker.io/binds/0/Source=/var/run/docker.sock,desktop.docker.io/binds/0/SourceKind=dockerSocketProxied,desktop.docker.io/binds/0/Target=/var/run/docker.sock","LocalVolumes":"0","Mounts":"/run/host-serv…","Names":"socat","Networks":"bridge","Ports":"127.0.0.1:2375-\u003e2375/tcp","RunningFor":"8 days ago","Size":"0B (virtual 6.95MB)","State":"running","Status":"Up 23 hours"}
9 | {"CreatedAt":"2021-08-27 11:14:06 -0400 EDT","ID":"5e92d0ef966e","Image":"bobrik/socat","Labels":"desktop.docker.io/binds/0/Source=/var/run/docker.sock,desktop.docker.io/binds/0/SourceKind=dockerSocketProxied,desktop.docker.io/binds/0/Target=/var/run/docker.sock","LocalVolumes":"0","Mounts":"/run/host-serv…","Names":"socat","Networks":"bridge","Ports":"127.0.0.1:2375-\u003e2375/tcp","RunningFor":"8 days ago","Size":"0B (virtual 6.95MB)","State":"running","Status":"Up 23 hours"}
10 | {"CreatedAt":"2021-08-27 11:14:06 -0400 EDT","ID":"5e92d0ef966e","Image":"bobrik/socat","Labels":"desktop.docker.io/binds/0/Source=/var/run/docker.sock,desktop.docker.io/binds/0/SourceKind=dockerSocketProxied,desktop.docker.io/binds/0/Target=/var/run/docker.sock","LocalVolumes":"0","Mounts":"/run/host-serv…","Names":"socat","Networks":"bridge","Ports":"127.0.0.1:2375-\u003e2375/tcp","RunningFor":"8 days ago","Size":"0B (virtual 6.95MB)","State":"running","Status":"Up 23 hours"}`
11 | it('should return an array', () => {
12 | expect(parseCliJSON(fakeData)).toBeInstanceOf(Array);
13 | expect(parseCliJSON(fakeMultilineData)).toBeInstanceOf(Array);
14 | })
15 | it('should work with single line data', () => {
16 | expect(parseCliJSON(fakeData)).toHaveLength(1);
17 | })
18 | it('should work with multi line data', () => {
19 | expect(parseCliJSON(fakeMultilineData)).toHaveLength(4);
20 | })
21 | })
22 | })
--------------------------------------------------------------------------------
/__tests__/reactTests.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/__tests__/reactTests.js
--------------------------------------------------------------------------------
/__tests__/supertest.js:
--------------------------------------------------------------------------------
1 |
2 | const supertest = require('supertest');
3 | const server = 'http://localhost:8080';
4 |
5 | const request = supertest(server);
6 |
7 | describe('Route integration', () => {
8 | describe('/graphql', () => {
9 | it('gets a list of users', (done) => {
10 | request
11 | .post('/graphql')
12 | .send({
13 | query: "{ users{ id, name} }",
14 | })
15 | .set("Accept", "application/json")
16 | .expect("Content-Type", "application/json; charset=utf-8")
17 | .expect(200)
18 | .end((err, res) => {
19 | if (err) done(err);
20 | expect(res.body).toBeInstanceOf(Object);
21 | })
22 | })
23 | })
24 | })
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useState } from 'react';
3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
4 | import Login from './components/authentication/Login';
5 | import DashboardContainer from './containers/DashboardContainer';
6 | import ContainersContainer from './containers/ContainersContainer';
7 | import NotificationsContainer from './containers/NotificationsContainer';
8 | import SettingsContainer from './containers/SettingsContainer';
9 | import NavBar from './components/NavBar/NavBar';
10 | import ProtectedRoute from './ProtectedRoute.jsx';
11 | import Form from './components/authentication/form'
12 | import './styles.scss';
13 | import { DndProvider } from 'react-dnd'
14 | import { HTML5Backend } from 'react-dnd-html5-backend'
15 |
16 | const App = () => {
17 | const [userId, setUserId] = useState('');
18 | const [auth, setAuth] = useState(false);
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | //if user tries to go to any other path that isn't defined
33 | "404 NOT FOUND"} />
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default App;
--------------------------------------------------------------------------------
/client/ProtectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | const jwt = require('jsonwebtoken')
3 | import { Route, Redirect } from "react-router-dom";
4 | import Cookies from 'js-cookie';
5 | import { useState, useEffect} from 'react';
6 |
7 | const ProtectedRoute = ({component: Component, ...rest }) => {
8 | return (
9 | {
12 | //if user is authenticated, return return respective component
13 | if(localStorage.getItem('validAuth'))
14 | return
15 | //else send back to login page
16 | else{
17 | return
18 | }
19 | }} />
20 | );
21 | }
22 |
23 | export default ProtectedRoute;
--------------------------------------------------------------------------------
/client/assets/delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/client/assets/delete.png
--------------------------------------------------------------------------------
/client/assets/logo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/client/assets/logo.gif
--------------------------------------------------------------------------------
/client/assets/logoNoText.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/client/assets/logoNoText.png
--------------------------------------------------------------------------------
/client/assets/logoTextOnly.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/client/assets/logoTextOnly.png
--------------------------------------------------------------------------------
/client/assets/restart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/client/assets/restart.png
--------------------------------------------------------------------------------
/client/assets/stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/client/assets/stop.png
--------------------------------------------------------------------------------
/client/assets/whaleBlue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/client/assets/whaleBlue.png
--------------------------------------------------------------------------------
/client/assets/whaleRed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/whalewatch/49b5e49036e3872329fbd034ccd958923cda67c4/client/assets/whaleRed.png
--------------------------------------------------------------------------------
/client/components/NavBar/NavBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Link} from 'react-router-dom'
3 | import logoNoText from '../../assets/logoNoText.png';
4 | import logoTextOnly from '../../assets/logoTextOnly.png';
5 | import logo from '../../assets/logo.gif';
6 |
7 | const NavBar = () => {
8 | const removeCookies = () => {
9 | localStorage.removeItem('validId');
10 | localStorage.removeItem('validAuth')
11 | }
12 |
13 | return (
14 |
15 |
44 |
45 | )
46 | }
47 |
48 | export default NavBar
--------------------------------------------------------------------------------
/client/components/authentication/Login.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import { Link, useHistory } from "react-router-dom";
4 | import logo from '../../assets/logo.gif';
5 | import Cookies from 'js-cookie';
6 | import {
7 | useMutation,
8 | gql
9 | } from '@apollo/client';
10 |
11 |
12 | const style = {
13 | signupOrLogin: {
14 | top: '-120px',
15 | right: '40px',
16 | color: '#0275d8',
17 | size: '15px',
18 | }
19 | };
20 |
21 | // mutation to use upon attempting to login
22 |
23 |
24 | // GraphQL login mutation query
25 | const LOGIN_MUTATION = gql`
26 | mutation login($username: String!, $password: String!){
27 | validateUser(username: $username, password: $password){
28 | id
29 | username
30 | password
31 | }
32 | }
33 | `;
34 |
35 | const Login = ({ setUserId }) => {
36 | const [userData, setUserData] = useState({ username: '', password: '' });
37 | const [errorMessage, setErrorMessage] = useState({ value: '' });
38 | const history = useHistory();
39 | //function to handle changing username
40 | const handleUsernameInputChange = (e) => {
41 | e.persist();
42 | setUserData((userData) => ({
43 | ...userData, username: e.target.value,
44 | }));
45 | };
46 | //function to handle changing password
47 | const handlePasswordInputChange = (e) => {
48 | e.persist();
49 | setUserData((userData) => ({
50 | ...userData, password: e.target.value,
51 | }));
52 | };
53 |
54 | //on submitting login info, invoke mutation to send to graphql
55 | const [login, { data, loading, error }] =
56 | useMutation(LOGIN_MUTATION, {
57 | variables: {
58 | username: userData.username,
59 | password: userData.password
60 | },
61 | onError: (err) => console.log('there is an error', err),
62 | onCompleted: (data) => {
63 | if (data.validateUser) {
64 | //if user validated, set a cookie using jwt from backend and store in localStorage
65 | Cookies.set('id', data.validateUser.id)
66 | localStorage.setItem('validId', data.validateUser.id)
67 | localStorage.setItem('validAuth', Cookies.get('access-token'))
68 | history.push('/dashboard')
69 | }
70 | }
71 | })
72 |
73 | return (
74 |
75 |
Don't have an account?
76 |
77 |
78 |
79 |
80 |
81 |
Welcome back! Please login.
82 |
83 |
118 |
119 |
120 |
121 |
122 |
123 | )
124 | }
125 |
126 | export default Login;
--------------------------------------------------------------------------------
/client/components/authentication/SignUpForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Validation from './validation.js';
3 | import { useMutation, gql } from '@apollo/client';
4 | import { Link, useHistory } from "react-router-dom";
5 | import logo from '../../assets/logo.gif';
6 | import Cookies from 'js-cookie';
7 |
8 |
9 | const style = {
10 | signupOrLogin: {
11 | top: '-120px',
12 | right: '40px',
13 | color: '#0275d8',
14 | size: '15px',
15 | }
16 | };
17 |
18 | const REGISTER_USER = gql`
19 | mutation addUser ($username: String!, $email: String!, $password: String!) {
20 | addUser(
21 | username: $username
22 | email: $email
23 | password: $password
24 | ){
25 | id
26 | username
27 | email
28 | password
29 | }
30 | }
31 | `;
32 |
33 | // create a sign-up form
34 | const SignUpForm = ({ submitForm }) => {
35 | const [inputValues, setValues] = useState({
36 | username: '',
37 | email: '',
38 | password: ''
39 | });
40 |
41 |
42 | const [errors, setErrors] = useState({});
43 | const [dataIsCorrect, setDataIsCorrect] = useState(false);
44 | const history = useHistory();
45 |
46 | // handle changes on the input fields
47 | const handleChange = (event) => {
48 | setValues({
49 | ...inputValues,
50 | [event.target.name]: event.target.value,
51 | });
52 | };
53 |
54 | const [addUser, { data, loading, error }] = useMutation(REGISTER_USER, {
55 | variables: inputValues,
56 | onCompleted: (data) => {
57 | if (data.addUser) {
58 | Cookies.set('id', data.addUser.id)
59 | localStorage.setItem('validId', data.addUser.id)
60 | localStorage.setItem('validAuth', Cookies.get('access-token'))
61 | history.push('/dashboard')
62 | }
63 | }
64 | });
65 |
66 | // handle submit button
67 | const handleFormSubmit = (event) => {
68 | event.preventDefault();
69 | setErrors(Validation(inputValues));
70 | setDataIsCorrect(true);
71 | addUser();
72 | };
73 |
74 | // handle error message for the input fields
75 | useEffect(() => {
76 | if (Object.keys(errors).length === 0 && dataIsCorrect) {
77 | submitForm(true);
78 | };
79 | }, [errors]);
80 |
81 | return (
82 |
83 |
Already have an account?
84 |
85 |
86 |
87 |
Welcome! Create Your Account.
88 |
111 |
112 |
113 |
114 | )
115 | }
116 |
117 |
118 |
119 | export default SignUpForm;
--------------------------------------------------------------------------------
/client/components/authentication/form.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import SignUpForm from './SignUpForm.js';
3 |
4 |
5 | const Form = (props) => {
6 | const [formIsSubmit, setFormIsSubmitted] = useState(false);
7 | const submitForm = () => {
8 | setFormIsSubmitted(true);
9 | };
10 | return (
11 |
12 |
13 |
14 | )
15 | };
16 | export default Form;
17 |
--------------------------------------------------------------------------------
/client/components/authentication/validation.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import SignUpForm from './SignUpForm.js';
3 |
4 | const Validation = (inputValues) => {
5 | let errors = {};
6 | //rules for validation
7 | if (!inputValues.username) {
8 | errors.username = "Username is required."
9 | }
10 | if (!inputValues.email) {
11 | errors.email = "Email is required."
12 | } else if (!/\S+@\S+\.\S+/.test(inputValues.email)) {
13 | errors.email = "Email is invalid."
14 | }
15 | if (!inputValues.password) {
16 | errors.password = "Password is required."
17 | } else if (inputValues.password.length < 5){
18 | errors.password = "Password must be more than 5 characters"
19 | }
20 | return errors;
21 | }
22 |
23 | export default Validation;
24 |
--------------------------------------------------------------------------------
/client/components/dashboard/AverageCPUChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import { Link, useHistory } from "react-router-dom";
4 | import { LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip } from 'recharts';
5 |
6 | const AverageCPUChart = ({ data, populateChart }) => {
7 |
8 | //invoking the function to process the data for recharts
9 | const dataArr = populateChart('cpuusage', data);
10 | return (
11 | <>
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | >
20 | )
21 | }
22 | export default AverageCPUChart;
--------------------------------------------------------------------------------
/client/components/dashboard/AverageMemoryChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import { Link, useHistory } from "react-router-dom";
4 | import { LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip } from 'recharts';
5 |
6 |
7 | const AverageMemoryChart = ({ data, populateChart }) => {
8 |
9 | //invoking the function to process the data for recharts
10 | const dataArr = populateChart('memusage', data);
11 | return (
12 | <>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | >
21 | )
22 |
23 | }
24 |
25 | export default AverageMemoryChart;
--------------------------------------------------------------------------------
/client/components/dashboard/BlockIOChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import { Link, useHistory } from "react-router-dom";
4 | import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip, Legend } from 'recharts';
5 | import formatBytes from "../../containers/containerHelpers";
6 |
7 | const BlockIOChart = ({ data, populateBarChart }) => {
8 |
9 | //invoking the function to process the data for recharts
10 | const dataArr = populateBarChart('blockio', data);
11 | return (
12 | <>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | >
23 | )
24 | }
25 |
26 | export default BlockIOChart;
--------------------------------------------------------------------------------
/client/components/dashboard/NetIOChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import { Link, useHistory } from "react-router-dom";
4 | import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip, Legend } from 'recharts';
5 | import formatBytes from "../../containers/containerHelpers";
6 |
7 | const NetIOChart = ({ data, populateBarChart }) => {
8 |
9 | //invoking the function to process the data for recharts
10 | const dataArr = populateBarChart('netio', data);
11 | return (
12 | <>
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | >
23 | )
24 |
25 | }
26 |
27 | export default NetIOChart;
--------------------------------------------------------------------------------
/client/components/dashboard/PIDChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import { Link, useHistory } from "react-router-dom";
4 | import { LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip } from 'recharts';
5 |
6 |
7 | const PIDChart = ({ data, populateChart }) => {
8 |
9 | //invoking the function to process the data for recharts
10 | const dataArr = populateChart('pids', data);
11 |
12 | return (
13 | <>
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | >
22 | )
23 | }
24 |
25 | export default PIDChart;
--------------------------------------------------------------------------------
/client/components/dashboard/Whale.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import whaleBlue from "../../assets/whaleBlue.png"
3 | import whaleRed from "../../assets/whaleRed.png"
4 | import { useDrag } from "react-dnd";
5 |
6 | //each container that is on the user's computer is represented by a whale
7 | const Whale = ({ info }) => {
8 |
9 | //use-drag hook from DnD
10 | const [{ isDragging }, drag] = useDrag(() => ({
11 | type: "image", //type can be a div if you want
12 | collect: (monitor) => ({
13 | isDragging: !!monitor.isDragging(),
14 | })
15 | }));
16 |
17 | return (
18 |
19 | {/* whale color is determined by whether the container is healthy or unhealthy */}
20 | {info.status !== 'unhealthy' ?
:
}
21 |
Container {info.name}
{info.size}
22 |
23 | )
24 | }
25 |
26 | export default Whale;
--------------------------------------------------------------------------------
/client/components/dashboard/WhaleChart.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import { Link, useHistory } from "react-router-dom";
4 | import Whale from "./Whale";
5 |
6 | const WhaleChart = ({ listOfContainers }) => {
7 |
8 | const list = listOfContainers.container
9 | const whales = [];
10 | list.map(container => {
11 | whales.push( );
12 | });
13 | return (
14 |
15 | {whales}
16 |
17 | )
18 | }
19 |
20 | export default WhaleChart;
--------------------------------------------------------------------------------
/client/components/dockerContainer/DndContainers.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import EachContainer from './EachContainer';
3 |
4 | const DndContainers = ({listOfContainers, state}) => {
5 |
6 | const allContainer = [];
7 | listOfContainers.map(container => {
8 | if(container.state === state){
9 | allContainer.push();
10 | }
11 | });
12 | return (
13 |
14 | {allContainer}
15 |
16 | )
17 | };
18 | export default DndContainers;
19 |
--------------------------------------------------------------------------------
/client/components/dockerContainer/EachContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDrag } from 'react-dnd'
3 |
4 | const EachContainer = ({info}) => {
5 | //for dragging
6 | const [{ isDragging }, drag] = useDrag(() => ({
7 | type: "image",
8 | item: {info: info.dockerid, state: info.state },
9 | collect: (monitor) => ({
10 | isDragging: !!monitor.isDragging(),
11 | }),
12 | }));
13 |
14 | const opacity = isDragging ? 0 : 1;
15 |
16 | return (
17 |
18 | Name: {info.name} Docker ID: {info.dockerid.slice(0, 12)} Status: {info.status} Container Size:{info.size}
19 |
20 | )
21 | }
22 |
23 | export default EachContainer;
--------------------------------------------------------------------------------
/client/components/dockerContainer/ItemTypes.jsx:
--------------------------------------------------------------------------------
1 | export const ItemTypes = {
2 | CONTAINER: 'container',
3 | }
--------------------------------------------------------------------------------
/client/components/dockerContainer/Restart.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDrop } from 'react-dnd';
3 | import stop from '../../assets/stop.png'
4 | import restart from '../../assets/restart.png'
5 |
6 | const style = {
7 | height: '8rem',
8 | width: '8rem',
9 | padding: '1rem',
10 | marginBottom: '1rem',
11 | textAlign: 'center',
12 | fontSize: '1rem',
13 | lineHeight: 'normal',
14 | float: 'left',
15 | };
16 |
17 | function Restart({containerData, state, refetch}) {
18 |
19 | const [{ isOver, canDrop }, drop] = useDrop({
20 | accept: 'image',
21 | drop: (item) => {
22 | fetch('/graphql', {
23 | method: 'POST',
24 | headers: {
25 | 'Content-Type': 'application/json',
26 | },
27 | body: JSON.stringify({
28 | query: `
29 | mutation restartContainer ($id: String){
30 | restartContainer(id: $id) {
31 | id
32 | }
33 | }`,
34 | variables: {
35 | id: item.info
36 | }
37 | })
38 | })
39 | .then(data => refetch())
40 | },
41 | collect: (monitor) => ({
42 | isOver: !!monitor.isOver(),
43 | canDrop: monitor.canDrop(),
44 | }),
45 | });
46 |
47 | const isActive = canDrop && isOver;
48 | let backgroundColor;
49 | if (isActive) {
50 | backgroundColor = 'lightblue';
51 | }
52 | return (
53 |
54 |
55 |
56 | )
57 | }
58 |
59 | export default Restart;
60 |
--------------------------------------------------------------------------------
/client/components/dockerContainer/Stop.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect} from 'react';
2 | import { useDrop } from 'react-dnd';
3 | import stop from '../../assets/stop.png'
4 | import restart from '../../assets/restart.png'
5 | import { useQuery, gql, useMutation} from '@apollo/client';
6 |
7 | const style = {
8 | height: '8rem',
9 | width: '8rem',
10 | marginRight: '4rem',
11 | marginBottom: '1rem',
12 | color: 'white',
13 | padding: '1rem',
14 | textAlign: 'center',
15 | fontSize: '1rem',
16 | lineHeight: 'normal',
17 | float: 'left',
18 | };
19 |
20 | function Stop({containerData, refetch}) {
21 |
22 | const [{ isOver, canDrop }, drop] = useDrop({
23 | accept: 'image',
24 | drop: (item) => {
25 | fetch('/graphql', {
26 | method: 'POST',
27 | headers: {
28 | 'Content-Type': 'application/json',
29 | },
30 | body: JSON.stringify({
31 | query: `
32 | mutation stopContainer ($id: String) {
33 | stopContainer(id: $id) {
34 | id
35 | }
36 | }`,
37 | variables: {
38 | id: item.info
39 | }
40 | })
41 | })
42 | .then(data => refetch())
43 | },
44 | collect: (monitor) => ({
45 | isOver: !!monitor.isOver(),
46 | canDrop: monitor.canDrop(),
47 | }),
48 | });
49 |
50 | const isActive = canDrop && isOver;
51 | let backgroundColor;
52 | if (isActive) {
53 | backgroundColor = '#c80004';
54 | }
55 | return (
56 |
57 |
58 |
59 | )
60 | }
61 |
62 | export default Stop;
63 |
--------------------------------------------------------------------------------
/client/components/notification/Notification.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Notification = (props) => {
4 | return (
5 | <>
6 | Alert! Your container is using too much memory. We recommend restarting the container.
7 | Alert! Your container is using too much CPU. We recommend restarting the container.
8 | >
9 | )
10 | }
11 |
12 | export default Notification;
--------------------------------------------------------------------------------
/client/containers/ContainersContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect} from 'react';
2 | import NavBar from '../components/NavBar/NavBar';
3 | import { useQuery, gql, useLazyQuery} from '@apollo/client';
4 | import Cookies from 'js-cookie';
5 | import deleteContainer from "../assets/delete.png";
6 | import stopContainer from "../assets/stop.png";
7 | import restartContainer from "../assets/restart.png";
8 | import DndContainers from "../components/dockerContainer/DndContainers"
9 | import Stop from "../components/dockerContainer/Stop"
10 | import Restart from '../components/dockerContainer/Restart'
11 |
12 | const GET_CONTAINERS = gql`
13 | query Containers ($id: Int) {
14 | container(id: $id) {
15 | id
16 | dockerid
17 | name
18 | size
19 | state
20 | status
21 | }
22 | }
23 | `;
24 |
25 |
26 | const ContainersContainer = ({validId}) => {
27 |
28 | const variables = { id: parseInt(localStorage.getItem('validId')) };
29 | const { loading, error, data, refetch } = useQuery(GET_CONTAINERS, {variables})
30 | const [containerData, setContainerData] = useState([])
31 |
32 | return (
33 |
34 |
35 |
36 |
Containers
37 |
38 | {/* Active Containers */}
39 |
40 |
41 | {/* */}
42 |
43 | {/* */}
44 |
Active Containers
45 |
46 | {/* */}
47 |
48 | {loading ?
Loading...
:
}
49 |
50 |
51 |
52 |
53 |
54 |
58 |
59 | {/* InActive Containers */}
60 |
61 |
62 | {/* */}
63 |
64 | {/* */}
65 |
Inactive Containers
66 |
67 | {/* */}
68 |
69 | {loading ?
Loading...
:
}
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | )
79 | }
80 |
81 | export default ContainersContainer;
82 |
83 |
--------------------------------------------------------------------------------
/client/containers/DashboardContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route, Redirect } from "react-router-dom";
3 | import { useState, useEffect } from 'react';
4 | import WhaleChart from "../components/dashboard/WhaleChart";
5 | import AverageCPUChart from "../components/dashboard/AverageCPUChart";
6 | import AverageMemoryChart from "../components/dashboard/AverageMemoryChart";
7 | import NetIOChart from "../components/dashboard/NetIOChart";
8 | import BlockIOChart from "../components/dashboard/BlockIOChart";
9 | import { useQuery, gql } from '@apollo/client';
10 | import NavBar from "../components/NavBar/NavBar";
11 | import PIDChart from "../components/dashboard/PIDChart";
12 | import Cookies from 'js-cookie';
13 | import formatBytes from "./containerHelpers";
14 |
15 | //get containers is a query that requests container data from our GraphQL endpoint
16 | const GET_CONTAINERS = gql`
17 | query Containers ($id: Int) {
18 | container(id: $id) {
19 | id
20 | dockerid
21 | name
22 | size
23 | status
24 | stats {
25 | timestamp
26 | cpuusage
27 | memusage
28 | netio
29 | blockio
30 | pids
31 | reqpermin
32 | }
33 | }
34 | }
35 | `;
36 |
37 | function forceRerender() {
38 | const [forceRerender, setForceRerender] = useState(0);
39 | return () => setForceRerender(forceRerender => forceRerender + 1)
40 | }
41 |
42 | const DashboardContainer = ({ validId }) => {
43 | const reRender = forceRerender();
44 | //currently, user id is held in local storage
45 | const variables = { id: Number(localStorage.getItem('validId')) };
46 | //we are using Apollo useQuery hooks to fetch the data
47 | const { loading, error, data } = useQuery(GET_CONTAINERS, { variables });
48 | if (error) return `Error! ${error.message}`;
49 |
50 | /*
51 | function to parse the data for the line charts
52 | inputs: datatype string, data object from graphql
53 | output: an array of data, formatted properly for recharts
54 | */
55 | const populateChart = (datatype, data) => {
56 | const array = data.container;
57 | const dataArr = [];
58 | const dataCache = {};
59 | //loop through the container data and add the appropriate data points to the cache (grouped by timestamp)
60 | array.forEach(container => {
61 | const stats = container.stats;
62 | stats.forEach(stat => {
63 | if (!dataCache[stat.timestamp]) {
64 | dataCache[stat.timestamp] = [];
65 | }
66 | dataCache[stat.timestamp].push(stat[datatype]);
67 | })
68 | })
69 | //loop through the cache and average the values at each timestamp
70 | Object.keys(dataCache).forEach(time => {
71 | let total = 0;
72 | dataCache[time].forEach(entry => total += entry);
73 | const avg = total / dataCache[time].length;
74 | let timestamp = Number(time);
75 | timestamp = new Date(timestamp)
76 | //timestamp (which is our x axis label) is currently the numerical date - would refactor to a time instead
77 | dataArr.push({ timestamp: timestamp.getDate(), datatype: avg.toFixed(2) });
78 | })
79 | //sort the resulting array by time
80 | dataArr.sort((a, b) => a.timestamp - b.timestamp)
81 | return dataArr;
82 | }
83 |
84 |
85 | /*function to parse data for the bar charts
86 | inputs: datatype string, data object from graphql
87 | output: an array of data, formatted properly for recharts
88 | in future, I'd like to refactor this to be more dry (one chart func with helper funcs)
89 | */
90 | const populateBarChart = (datatype, data) => {
91 | const arr = data.container;
92 | const dataArr = [];
93 | const dataCache = {};
94 | //loop through the container data and add the appropriate data points to the cache (grouped by timestamp)
95 | arr.forEach(container => {
96 | const stats = container.stats;
97 | stats.forEach(stat => {
98 | if (!dataCache[stat.timestamp]) {
99 | dataCache[stat.timestamp] = [];
100 | }
101 | dataCache[stat.timestamp].push(stat[datatype]);
102 | })
103 | })
104 | //loop through the cache and average the values at each timestamp
105 | Object.keys(dataCache).forEach(time => {
106 | const timeArr = dataCache[time];
107 | const inputArr = [];
108 | const outputArr = [];
109 | timeArr.forEach(el => {
110 | const idx = el.indexOf('B');
111 | //the bar chart data is in strings with numbers of bytes, so we need to slice up to grab input and output numbers
112 | inputArr.push(el.slice(0, idx));
113 | const out = el.slice(idx + 4, -1)
114 | outputArr.push(out === '' ? '0' : out)
115 | })
116 | //averaging input and output
117 | const totalIn = inputArr.reduce((a, c) => Number(a) + Number(c))
118 | const totalOut = outputArr.reduce((a, c) => Number(a) + Number(c))
119 | let avgIn = totalIn / inputArr.length;
120 | avgIn = isNaN(avgIn) ? 0 : formatBytes(avgIn);
121 | let avgOut = totalOut / outputArr.length;
122 | avgOut = isNaN(avgOut) ? 0 : formatBytes(avgOut);
123 | let timestamp = Number(time);
124 | timestamp = new Date(timestamp)
125 | //pushing data in the correct format for recharts to the output array
126 | dataArr.push({ name: timestamp.getDate(), in: avgIn, out: avgOut })
127 | })
128 | //sorting by date
129 | dataArr.sort((a, b) => a.name - b.name)
130 | return dataArr;
131 | }
132 |
133 |
134 |
135 | return (
136 |
137 |
138 |
139 |
142 |
143 | {/* Whale Chart */}
144 |
145 | {/* */}
146 |
147 | {/* */}
148 |
Container Health Overview
149 |
Refresh
150 |
151 | {/* */}
152 |
153 | {/* */}
154 | {loading ?
Loading...
:
}
155 |
156 |
157 |
158 | {/* AverageCPUChart */}
159 |
160 |
161 |
Average CPU Usage
162 |
163 |
164 | {loading ?
Loading...
:
}
165 |
166 |
167 |
168 | {/* AverageMemoryChart */}
169 |
170 |
171 |
Average Memory Usage
172 |
173 |
174 | {loading ?
Loading...
:
}
175 |
176 |
177 |
178 | {/* Average Net I/O */}
179 |
180 |
181 |
Average Net I/O
182 |
183 |
184 | {loading ?
Loading...
:
}
185 |
186 |
187 |
188 | {/* BlockIOChart */}
189 |
190 |
191 |
Average Block I/O
192 |
193 |
194 | {loading ?
Loading...
:
}
195 |
196 |
197 |
198 | {/* PIDChart */}
199 |
200 |
203 |
204 | {loading ?
Loading...
:
}
205 |
206 |
207 |
208 |
209 | )
210 | }
211 |
212 | export default DashboardContainer
213 |
214 |
--------------------------------------------------------------------------------
/client/containers/NotificationsContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import NavBar from "../components/NavBar/NavBar";
4 | import Notification from "../components/notification/Notification";
5 |
6 | const NotificationsContainer = (props) => {
7 | return (
8 |
9 |
10 |
11 |
Notifications
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default NotificationsContainer
--------------------------------------------------------------------------------
/client/containers/SettingsContainer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from 'react';
3 | import NavBar from "../components/NavBar/NavBar";
4 |
5 | const SettingsContainer = (props) => {
6 |
7 | return (
8 |
9 |
10 |
11 |
Settings
12 |
Here are the settings.
13 |
14 |
15 | )
16 | }
17 |
18 | export default SettingsContainer
19 |
--------------------------------------------------------------------------------
/client/containers/containerHelpers.js:
--------------------------------------------------------------------------------
1 | /*
2 | input: integer and optional decimals integer
3 | output: float
4 | this function formats bytes to kilobytes for a cleaner display
5 | */
6 | function formatBytes(bytes, decimals = 2) {
7 | if (bytes === 0) return '0 Bytes';
8 |
9 | const k = 1024;
10 | const dm = decimals < 0 ? 0 : decimals;
11 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
12 |
13 | return parseFloat((bytes / Math.pow(k, 1)).toFixed(dm)) // + ' ' + sizes[i];
14 | }
15 |
16 | export default formatBytes
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 | WhaleWatch
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App.jsx';
4 | import {
5 | ApolloClient,
6 | InMemoryCache,
7 | ApolloProvider,
8 | } from "@apollo/client";
9 | import styles from './scss/Application.scss';
10 |
11 | const enhancedFetch = (url, init) => {
12 |
13 | return fetch(url, {
14 | ...init,
15 | headers: {
16 | ...init.headers,
17 | 'Access-Control-Allow-Origin': '*'
18 | },
19 | },
20 | ).then(response => response)
21 | }
22 | //set up of our apollo client
23 | const client = new ApolloClient({
24 | uri: '/graphql',
25 | cache: new InMemoryCache(),
26 | fetchOptions: {
27 | mode: 'no-cors',
28 | credentials: 'include'
29 | },
30 | fetch: enhancedFetch,
31 | });
32 |
33 | ReactDOM.render(
34 |
35 |
36 | ,
37 | document.getElementById('root'));
--------------------------------------------------------------------------------
/client/scss/Application.scss:
--------------------------------------------------------------------------------
1 | @import 'ContainersContainer';
2 |
3 |
--------------------------------------------------------------------------------
/client/scss/ContainersContainer.scss:
--------------------------------------------------------------------------------
1 | $containerBlue:rgb(98, 189, 222);
2 |
3 | #allContainers{
4 | display: flex;
5 | height: 100%;
6 | }
7 |
8 | #containersPage{
9 | width: 65%;
10 | display: flex;
11 | flex-direction: column;
12 | align-items: center;
13 | margin-left: 200px;
14 | }
15 |
16 | #containersList{
17 | padding: 25px;
18 | margin: 10px;
19 | font-size:16px;
20 | border: solid lightblue .75px;
21 | align-content: space-between;
22 | width: 275px;
23 | border-radius: 20px;
24 | box-shadow: 6px 6px 6px 3px rgb(250, 246, 246);
25 | }
26 |
27 |
28 |
29 | #container-list-dnd{
30 | display: flex;
31 | flex-direction: row;
32 | flex-wrap: wrap;
33 | border-radius: 5px;
34 | }
35 |
36 | #containersList > li{
37 | margin: 0px;
38 | }
39 | .value{
40 | display: inline;
41 | }
42 |
43 |
44 | #actions{
45 | display: flex;
46 | width: 35%;
47 | justify-content: center;
48 | align-items: center;
49 | height: 100vh;
50 |
51 | }
52 |
53 | .containers{
54 | margin: 30px 0 30px 0;
55 | display: flex;
56 | flex-direction: row;
57 | justify-content: center;
58 | width: 80%;
59 | height: 50%;
60 | align-items: flex-start;
61 | }
62 | #icons{
63 | display: flex;
64 | flex-direction: column;
65 | width: 50%;
66 | align-items: center;
67 | border: 2px solid $containerBlue;
68 | padding: 20px;
69 | border-radius: 10%;
70 | }
71 |
72 | #icons > img {
73 | margin-bottom: 10px;
74 | }
75 |
76 | .containerTitle{
77 | margin-top: 30px;
78 | }
79 |
80 | .value{
81 | color: white;
82 | }
83 |
84 | #inactiveContainer{
85 | height: 50%;
86 | }
87 |
88 |
89 | // Re-edited
90 |
91 | .dnd-board {
92 | height:50%;
93 | }
94 |
95 | .card2{
96 | display: flex;
97 | flex-direction: column;
98 | // align-items: center;
99 | // border: 1px solid rgba(180, 74, 74, 0.05);
100 | background-color: rgb(255, 255, 255);
101 | background-clip: border-box;
102 | width:80%;
103 | // margin-left: 15px;
104 | // box-shadow: 0px 5px 5px 5px rgb(209, 209, 209);
105 | border-radius: 0px 0px 15px 15px ;
106 | margin-bottom: 30px;
107 | }
108 |
109 |
110 | .restart-stop {
111 | display:flex;
112 | margin-left: 20rem;
113 | align-items: center;
114 | // justify-content: center;
115 | div {
116 | margin-left: 1rem;
117 | margin-right: 1rem;
118 | margin-top: 0;
119 | margin-bottom: 0;
120 | }
121 | }
122 |
123 |
124 |
--------------------------------------------------------------------------------
/client/styles.scss:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | .welcome {
7 | margin-top: 8%;
8 | margin-bottom:1%;
9 | font-size: 23px;
10 | color:#404253;
11 | font-family: 'Nunito Sans', sans-serif;
12 | font-weight: 700;
13 | }
14 |
15 | // logo real size 396 × 302
16 | .logo{
17 | margin-top: 8%;
18 | margin-bottom: 3%;
19 | width: 138.6px;
20 | height: 105.7px;
21 | }
22 |
23 | .error-message{
24 | color: rgb(204, 84, 84)
25 | }
26 |
27 |
28 | .authen-box{
29 | display: flex;
30 | flex-direction: column;
31 | align-items: center;
32 | // background-color:#EEF2F8;
33 | height: 100%;
34 | margin-top:3%;
35 | }
36 |
37 |
38 | .authen-box-color{
39 | margin-top: 40px;
40 | display: flex;
41 | flex-direction: column;
42 | align-items: center;
43 | width:600px;
44 | height:700px;
45 |
46 | }
47 |
48 | // Nav Bar Section
49 | // .vertical-nav{
50 | // width: 200px;
51 | // height: 100vh;
52 | // background-color: white;
53 | // display: flex;
54 | // flex-direction: column;
55 | // align-items: center;
56 | // // border-radius: 15px;
57 | // box-shadow: 5px 5px 5px 1px rgb(207, 204, 204);
58 | // }
59 |
60 | .vertical-nav{
61 | width: 210px;
62 | height: 100%;
63 | background-color: white;
64 | display: flex;
65 | flex-direction: column;
66 | align-items: center;
67 | box-shadow: 5px 5px 5px 1px rgb(207, 204, 204);
68 | position:fixed;
69 | }
70 |
71 | //logo real size: 396 × 302 (25%)
72 | .logo-small{
73 | margin-top:50px;
74 | margin-bottom: 10%;
75 | width: 99px;
76 | height: 75.5px;
77 | position: fixed;
78 | }
79 |
80 | .menulist{
81 | margin-bottom: 200px;
82 | }
83 |
84 | li {
85 | margin-top: 40px;
86 | width: 100%;
87 | }
88 |
89 | i {
90 | margin-right: 8px;
91 | }
92 |
93 | a:link {
94 | color:#404253;
95 | font-family: 'Nunito Sans', sans-serif;
96 | font-weight: 500;
97 | font-size:16px;
98 | text-decoration: none;
99 | position: fixed;
100 | margin-top: 150px;
101 | margin-left: -50px;
102 | }
103 |
104 | a:visited {
105 | color:#404253;
106 | font-family: 'Nunito Sans', sans-serif;
107 | font-weight: 500;
108 | font-size:16px;
109 | text-decoration: none;
110 | position: fixed;
111 | margin-top: 150px;
112 | margin-left: -50px;
113 | }
114 |
115 | a:hover {
116 | color:#48A1F8;
117 | font-family: 'Nunito Sans', sans-serif;
118 | font-weight: 500;
119 | font-size:16px;
120 | text-decoration: none;
121 | position: fixed;
122 | margin-top: 150px;
123 | margin-left: -50px;
124 | }
125 |
126 | .version {
127 | color:#404253;
128 | font-family: 'Nunito Sans', sans-serif;
129 | font-weight: 500;
130 | font-size:0.8em;
131 | position: fixed;
132 | bottom: 15px;
133 | }
134 |
135 |
136 | // Data Section
137 | .dashbaordContainer {
138 | background-color: #EEF2F8;
139 | display: flex;
140 | flex-direction: row;
141 | align-items: stretch;
142 | }
143 |
144 | .dashbaordData{
145 | align-items: center;
146 | margin-left:250px;
147 | min-height:50rem;
148 | }
149 |
150 | // test
151 | .card1{
152 | background-color: rgb(255, 255, 255);
153 | background-clip: border-box;
154 | width:1200px;
155 | border-radius: 0px 0px 15px 15px ;
156 | margin-bottom: 30px;
157 | }
158 |
159 | .top-card-header{
160 | background-color: rgb(255, 255, 255);
161 | border-radius: 25px;
162 | display: flex;
163 | flex-direction: row;
164 | }
165 |
166 | .card-header{
167 | background-color: rgb(255, 255, 255);
168 | border-radius: 25px;
169 | }
170 |
171 |
172 | #whale-chart{
173 | display: flex;
174 | flex-direction: row;
175 | flex-wrap: wrap;
176 | }
177 |
178 | .whale-display {
179 | padding: 25px;
180 | margin: 10px;
181 | font-size:16px;
182 | border: solid lightblue .75px;
183 | align-content: space-between;
184 | width: 275px;
185 | border-radius: 20px;
186 | box-shadow: 6px 6px 6px 3px rgb(250, 246, 246);
187 | }
188 |
189 | // whale display 126 x 103
190 | .whale{
191 | width: 75.6px;
192 | height: 61.8px;
193 | }
194 |
195 | .metric-type {
196 | color:#404253;
197 | font-family: 'Nunito Sans', sans-serif;
198 | font-weight: 650;
199 | font-size:18px;
200 | margin:3.5px;
201 | }
202 |
203 | .top-metric-type {
204 | color:#404253;
205 | font-family: 'Nunito Sans', sans-serif;
206 | font-weight: 650;
207 | font-size:18px;
208 | margin:3.5px;
209 | display: flex;
210 | justify-content: space-between;
211 | }
212 |
213 | .dashbaord-header {
214 | margin-top:30px;
215 | margin-bottom: 20px;
216 | color:#404253;
217 | font-family: 'Nunito Sans', sans-serif;
218 | font-weight: 800;
219 | font-size:1.2em;
220 | }
221 |
222 | // Settings
223 | .settingsContainer {
224 | display: flex;
225 | flex-direction: row;
226 | align-items: stretch;
227 |
228 | }
229 | .refresh-button {
230 | margin-top:5px;
231 | margin-bottom: 5px;
232 | font-family: 'Nunito Sans', sans-serif;
233 | color: white;
234 | background-color: #159ce4;
235 | border-radius: 10px;
236 | border: 0px;
237 | width: 120px;
238 | height:45px;
239 | font-size:20px;
240 | }
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 | const path = require('path');
3 | const url = require('url');
4 |
5 | // SET ENV
6 | process.env.NODE_ENV = 'development';
7 |
8 | const { app, BrowserWindow, Menu, ipcMain } = electron;
9 |
10 | let mainWindow;
11 | let addWindow;
12 |
13 | // listen for app to be ready
14 | app.on('ready', function () {
15 | // Create new window
16 | mainWindow = new BrowserWindow({
17 | webPreferences: {
18 | nodeIntegration: true,
19 | }
20 | });
21 | // Load HTML file into the window
22 | console.log(__dirname)
23 | mainWindow.loadURL('http://localhost:8080');
24 | })
25 |
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "whalewatch",
3 | "version": "1.0.0",
4 | "description": "WhaleWatch is a health monitoring app for Docker containers.",
5 | "main": "main.js",
6 | "scripts": {
7 | "build": "NODE_ENV=production webpack",
8 | "start": "NODE_ENV=production node server/index.js",
9 | "dev": "NODE_ENV=development webpack serve & NODE_ENV=development nodemon ./server/index.js",
10 | "electron": "electron .",
11 | "test": "jest --verbose"
12 | },
13 | "nodemonConfig": {
14 | "ignore": [
15 | "build",
16 | "client"
17 | ]
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/oslabs-beta/whalewatch.git"
22 | },
23 | "author": "Phil Kang, Annie Pan, Rachel Patterson, and Matilda Wang",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/oslabs-beta/whalewatch/issues"
27 | },
28 | "homepage": "https://github.com/oslabs-beta/whalewatch#readme",
29 | "dependencies": {
30 | "@apollo/client": "^3.4.8",
31 | "apollo-server": "^3.3.0",
32 | "bcrypt": "^5.0.1",
33 | "cookie-parser": "^1.4.5",
34 | "dotenv": "^8.6.0",
35 | "dotenv-webpack": "^7.0.3",
36 | "electron": "^13.2.2",
37 | "express": "^4.17.1",
38 | "express-graphql": "^0.12.0",
39 | "express-jwt": "^6.1.0",
40 | "graphql": "^15.5.1",
41 | "html-webpack-plugin": "^5.3.2",
42 | "js-cookie": "^3.0.0",
43 | "jsonwebtoken": "^8.5.1",
44 | "node-fetch": "^2.6.1",
45 | "node-polyfill-webpack-plugin": "^1.1.4",
46 | "path": "^0.12.7",
47 | "pg": "^8.7.1",
48 | "pg-native": "^3.0.0",
49 | "react": "^17.0.2",
50 | "react-bootstrap": "^1.6.1",
51 | "react-dnd": "^14.0.3",
52 | "react-dnd-html5-backend": "^14.0.1",
53 | "react-dom": "^17.0.2",
54 | "react-router-dom": "^5.2.0",
55 | "recharts": "^2.1.2",
56 | "sass": "^1.38.0",
57 | "sequelize": "^6.6.5",
58 | "strip-ansi": "3.0.1",
59 | "webpack": "^5.50.0"
60 | },
61 | "devDependencies": {
62 | "@babel/core": "^7.15.0",
63 | "@babel/preset-env": "^7.15.0",
64 | "@babel/preset-react": "^7.14.5",
65 | "@testing-library/react": "^12.0.0",
66 | "babel-loader": "^8.2.2",
67 | "concurrently": "^6.2.1",
68 | "css-loader": "^6.2.0",
69 | "eslint": "^7.32.0",
70 | "jest": "^27.0.6",
71 | "nodemon": "^2.0.12",
72 | "postcss-loader": "^6.1.1",
73 | "sass-loader": "^12.1.0",
74 | "style-loader": "^3.2.1",
75 | "supertest": "^6.1.4",
76 | "ts-loader": "^9.2.5",
77 | "typescript": "^4.3.5",
78 | "webpack-cli": "^4.8.0",
79 | "webpack-dev-server": "^4.0.0"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/server/db/connect.js:
--------------------------------------------------------------------------------
1 | //setting up connection to the database
2 | require('dotenv').config();
3 | const { Pool } = require('pg');
4 | const uri = process.env.DB_URI;
5 | const pool = new Pool({
6 | connectionString: uri,
7 | max: 3
8 | });
9 |
10 | module.exports = pool;
--------------------------------------------------------------------------------
/server/db/schema.js:
--------------------------------------------------------------------------------
1 | const pool = require('./connect.js');
2 | const bcrypt = require('bcrypt');
3 | const graphql = require('graphql');
4 | const dbHelper = require('../helpers/dbHelper');
5 |
6 | const {
7 | GraphQLInt,
8 | GraphQLObjectType,
9 | GraphQLString,
10 | GraphQLID, // more flexible to use for IDs than strings
11 | GraphQLSchema,
12 | GraphQLList,
13 | GraphQLFloat,
14 | GraphQLScalarType,
15 | Kind
16 | } = graphql;
17 |
18 | const jwt = require('jsonwebtoken')
19 | //graphqlobjecttype is function from graphql
20 | //schema for Users
21 | const UserType = new GraphQLObjectType({
22 | name: 'User',
23 | description: 'Users data in our app',
24 | fields: () => ({
25 | id: {
26 | type: GraphQLInt,//resolve is what is invoked so graphql knows how to get data
27 | },
28 | username: { type: GraphQLString },
29 | email: { type: GraphQLString },
30 | password: { type: GraphQLString },
31 | containerName: {
32 | type: new GraphQLList(ContainerType),
33 | resolve: async (parent) => {
34 | await dbHelper.refreshContainerData(parent.id);
35 | const vals = [parent.id];
36 | const query = `SELECT * from containers WHERE owner=$1`;
37 | //return containerData.filter(container => container.user_id === parent.id);
38 | const res = await pool.query(query, vals);
39 | return res.rows;
40 | }
41 | },
42 | })
43 | })
44 |
45 | //schema for containers
46 | const ContainerType = new GraphQLObjectType({
47 | name: 'Containers',
48 | description: 'Our containers',
49 | fields: () => ({
50 | id: { type: GraphQLInt },
51 | dockerid: { type: GraphQLString },
52 | name: { type: GraphQLString },
53 | size: { type: GraphQLString },
54 | status: { type: GraphQLString },
55 | state: { type: GraphQLString },
56 | stats: {
57 | type: new GraphQLList(StatsType),
58 | resolve: async (parent) => {
59 | const vals = [parent.id];
60 | const query = `SELECT * from stats WHERE container=$1`;
61 | const res = await pool.query(query, vals);
62 | return res.rows;
63 | }
64 | }
65 | })
66 | })
67 |
68 | const StatsType = new GraphQLObjectType({
69 | name: 'Stats',
70 | description: 'Stats data',
71 | fields: () => ({
72 | id: { type: GraphQLID },
73 | container: { type: GraphQLID },
74 | timestamp: { type: GraphQLFloat }, // dates and times have to be defined as custom scalars like Date or timestamp - might need to npm install --save graphql-scalars
75 | cpuusage: { type: GraphQLFloat },
76 | memusage: { type: GraphQLFloat },
77 | netio: { type: GraphQLString },
78 | blockio: { type: GraphQLString },
79 | pids: { type: GraphQLInt },
80 | reqpermin: { type: GraphQLInt },
81 | })
82 | })
83 |
84 | // root query: how we initially jump into graph
85 | const RootQueryType = new GraphQLObjectType({
86 | name: 'RootQueryType',
87 | description: 'Root Query',
88 | fields: () => ({
89 | user: {
90 | type: new GraphQLList(UserType),
91 | //if this were a database, return database here
92 | resolve: async () => {
93 | const res = await pool.query(`SELECT * from "users"`);
94 | return res.rows;
95 | },
96 | },
97 | oneUser: {
98 | type: UserType,
99 | description: 'A single user',
100 | args: {
101 | id: { type: GraphQLInt }
102 | },
103 | resolve: async (parent, args) => {
104 | const vals = [args.id]
105 | const query = `SELECT * from "users" WHERE id = $1`
106 | const res = await pool.query(query, vals);
107 | return res.rows[0];
108 | },
109 | },
110 | container: {
111 | type: new GraphQLList(ContainerType),
112 | description: 'List of all our containers',
113 | args: {
114 | id: { type: GraphQLInt }
115 | },
116 | resolve: async (parent, args) => {
117 | await dbHelper.refreshContainerData(args.id);
118 | const res = await pool.query(`SELECT * from "containers" WHERE owner = $1`, [args.id]);
119 | return res.rows;
120 | }
121 | },
122 | oneContainer: {
123 | type: ContainerType,
124 | description: 'a single container',
125 | args: {
126 | container_id: { type: GraphQLInt }
127 | },
128 | resolve: async (parent, args) => {
129 | const vals = [args.id]
130 | const query = `SELECT * from "containers" WHERE id = $1`
131 | const res = await pool.query(query, vals);
132 | return res.rows[0];
133 | }
134 | },
135 | stat: {
136 | type: new GraphQLList(StatsType),
137 | description: 'Stats on a container',
138 | resolve: async () => {
139 | const res = await pool.query(`SELECT * from "stats"`);
140 | return res.rows;
141 | }
142 | }
143 | })
144 | })
145 |
146 | //mutation functions
147 | const RootMutationType = new GraphQLObjectType({
148 | name: 'Mutations',
149 | description: 'Manipulation of user data',
150 | fields: () => ({
151 | //functionality to add user into database
152 | addUser: {
153 | type: UserType,
154 | description: 'Add a user',
155 | args: {
156 | id: { type: GraphQLInt },//resolve is what is invoked so graphql knows how to get data
157 | username: { type: GraphQLString },
158 | email: { type: GraphQLString },
159 | password: { type: GraphQLString },
160 |
161 | },
162 | resolve: async (parent, args) => {
163 | //hash and salt returning pw
164 | const password = await bcrypt.hash(args.password, 10);
165 | const user = [args.username, args.email, password]
166 | //insert into database user information
167 | const query = 'INSERT INTO users (username, email, password) VALUES ($1, $2, $3) RETURNING *'
168 | const res = await pool.query(query, user);
169 | return res.rows[0];
170 | }
171 | },
172 | validateUser: {
173 | type: UserType,
174 | description: 'Make sure username + pw match',
175 | args: {
176 | username: { type: GraphQLString },
177 | password: { type: GraphQLString },
178 | },
179 | resolve: async (parent, args, { req, res }) => {
180 |
181 | const username = [args.username]
182 | const query = `SELECT password from users WHERE username = $1`;
183 | const result = await pool.query(query, username)
184 | const comparingPassword = await bcrypt.compare(args.password, result.rows[0].password);
185 | if (comparingPassword === true) {
186 | const finalResult = await pool.query(`SELECT * from users where username = $1`, username);
187 | const accessToken = jwt.sign({ userId: finalResult.rows[0].id }, 'Dockerpalsarecuties', { expiresIn: '15min' });
188 | const refreshToken = jwt.sign({ userId: finalResult.rows[0].id }, 'Dockerpalsarecuties', { expiresIn: '7d' });
189 | const refresh = res.cookie('refresh-token', refreshToken, { expire: 60 * 60 * 27 * 7 })
190 | const access = res.cookie('access-token', accessToken, { expire: 60 * 15 })
191 | return finalResult.rows[0];
192 |
193 | }
194 | }
195 | },
196 | addStat: {
197 | type: StatsType,
198 | description: 'Adding dummy stats to database',
199 | args: {
200 | id: { type: GraphQLID },
201 | container: { type: GraphQLInt },
202 | timestamp: { type: GraphQLFloat }, // dates and times have to be defined as custom scalars like Date or timestamp - might need to npm install --save graphql-scalars
203 | cpuusage: { type: GraphQLFloat },
204 | memusage: { type: GraphQLFloat },
205 | netio: { type: GraphQLString },
206 | blockio: { type: GraphQLString },
207 | pids: { type: GraphQLInt },
208 | reqpermin: { type: GraphQLInt },
209 | },
210 | resolve: async (parent, args) => {
211 | // args.container, args.timestamp, args.cpuusage, args.memusage, args.netio, args.blockio, args.pids, args.reqpermin
212 | const statEntry = [args.container, args.timestamp, args.cpuusage, args.memusage, args.netio, args.blockio, args.pids, args.reqpermin]
213 | const query = 'INSERT INTO stats (container, timestamp, cpuusage, memusage, netio, blockio, pids, reqpermin) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)'
214 | const res = await pool.query(query, statEntry);
215 | return res.rows[0];
216 | }
217 | },
218 | stopContainer: {
219 | type: ContainerType,
220 | description: 'Stop a container',
221 | args: {
222 | id: { type: GraphQLString }
223 | },
224 | resolve: (parent, args) => {
225 | dbHelper.stopContainer(args.id);
226 | return args.id;
227 | }
228 | },
229 | restartContainer: {
230 | type: ContainerType,
231 | description: 'Restart a container',
232 | args: {
233 | id: { type: GraphQLString }
234 | },
235 | resolve: async (parent, args) => {
236 | await dbHelper.restartContainer(args.id);
237 | return args.id;
238 | }
239 | }
240 | })
241 | });
242 |
243 | const schema = new GraphQLSchema({
244 | query: RootQueryType,
245 | mutation: RootMutationType
246 | })
247 |
248 | // export schema
249 | module.exports = schema;
250 |
251 |
252 |
253 |
--------------------------------------------------------------------------------
/server/db/setup.sql:
--------------------------------------------------------------------------------
1 | -- tables to create for database setup
2 | CREATE TABLE IF NOT EXISTS users (
3 | id SERIAL PRIMARY KEY,
4 | username varchar(50) UNIQUE NOT NULL,
5 | email varchar(100) UNIQUE NOT NULL,
6 | password varchar(100) NOT NULL
7 | )
8 |
9 | CREATE TABLE IF NOT EXISTS containers (
10 | id SERIAL PRIMARY KEY,
11 | dockerId varchar(100) NOT NULL,
12 | name varchar(100) NOT NULL,
13 | size varchar(50),
14 | status varchar(50),
15 | state varchar(50),
16 | owner integer REFERENCES users(id) NOT NULL
17 | )
18 |
19 | CREATE TABLE IF NOT EXISTS stats (
20 | id SERIAL PRIMARY KEY,
21 | container integer REFERENCES containers(id) NOT NULL,
22 | timestamp date NOT NULL,
23 | cpuUsage decimal NOT NULL,
24 | memUsage decimal NOT NULL,
25 | netIo varchar(50) NOT NULL,
26 | blockIo varchar(50) NOT NULL,
27 | pids integer NOT NULL,
28 | reqPerMin integer
29 | )
30 |
31 | module.exports = tables;
--------------------------------------------------------------------------------
/server/helpers/dbHelper.js:
--------------------------------------------------------------------------------
1 | const dockerApiHelper = require('./dockerApiHelper');
2 | const pool = require('../db/connect.js');
3 | const { container } = require('webpack');
4 | const dbHelper = {};
5 | const { dockerCliHelper } = require('./dockerCliHelper');
6 |
7 | //these functions will parse the docker data and add it to the database if not already there
8 |
9 | //this function takes in a given container's docker ID and database ID and queries for stats for the container, then adds them to the database
10 | const refreshStats = async (dockerId, id) => {
11 | try {
12 | const stats = await dockerCliHelper.getStats(dockerId);
13 | let timestamp = new Date(Date.now());
14 | const cpuUsage = stats[0].CPUPerc.slice(0, -1);
15 | const memUsage = stats[0].MemPerc.slice(0, -1);
16 | const netIo = stats[0].NetIO
17 | const blockIo = stats[0].BlockIO;
18 | const pids = stats[0].PIDs;
19 | const vals = [id, timestamp, cpuUsage, memUsage, netIo, blockIo, pids]
20 | await pool.query('INSERT INTO STATS (container, timestamp, cpuUsage, memUsage, netIo, blockIo, pids) VALUES ($1, $2, $3, $4, $5, $6, $7)', vals)
21 | } catch (err) {
22 | console.log('error in refreshing stats data', err)
23 | }
24 | }
25 |
26 | //this function takes in a user's database id and retrieves a list of all containers on their computer.
27 | //it then either updates or inserts each container
28 | dbHelper.refreshContainerData = async (owner) => {
29 | try {
30 | const containers = await dockerCliHelper.getContainerList();
31 | const insertQuery = 'INSERT INTO containers (dockerId, name, size, state, owner, status) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id';
32 | for (let container of containers) {
33 | const dockerId = container.ID;
34 | const name = container.Names;
35 | const size = container.Size;
36 | const state = container.State;
37 | let status;
38 |
39 | if (container.Status.includes('unhealthy')) {
40 | status = 'unhealthy';
41 | } else status = 'healthy';
42 | //check if already there, if so, update size and state
43 | const checkContainer = await pool.query('SELECT * from containers WHERE dockerId=$1 AND owner=$2', [dockerId, owner]);
44 | if (checkContainer.rows.length) {
45 | await pool.query('UPDATE containers SET size=$1, state=$2, status=$3 WHERE dockerId=$4 AND owner=$5', [size, state, status, dockerId, owner]);
46 | if (state === 'running') {
47 | await refreshStats(dockerId, checkContainer.rows[0].id);
48 | }
49 | } else {
50 | const dbId = await pool.query(insertQuery, [dockerId, name, size, state, owner, status]);
51 | if (state === 'running') {
52 | await refreshStats(dockerId, dbId.rows[0].id);
53 | }
54 | }
55 | }
56 | } catch (err) {
57 | console.log('error in refreshing container data', err);
58 | }
59 | }
60 |
61 | //this function stops a container by docker id and updates the database with its stopped state
62 | dbHelper.stopContainer = async (id) => {
63 | try {
64 | await dockerCliHelper.stopContainer(id);
65 | const stopQuery = 'UPDATE containers SET state=$1 WHERE dockerid=$2';
66 | await pool.query(stopQuery, ['exited', id]);
67 | } catch (err) { console.log(err) }
68 | }
69 |
70 | //this function restarts a container by docker id and updates the database with its running state
71 | dbHelper.restartContainer = async (id) => {
72 | try {
73 | await dockerCliHelper.restartContainer(id);
74 | const stopQuery = 'UPDATE containers SET state=$1 WHERE dockerid=$2';
75 | await pool.query(stopQuery, ['running', id]);
76 | } catch (err) { console.log(err) }
77 | }
78 |
79 | module.exports = dbHelper;
--------------------------------------------------------------------------------
/server/helpers/dockerApiHelper.js:
--------------------------------------------------------------------------------
1 | //this file contains functions to use the docker engine API. we have refactored and are no longer using these
2 | //however, I have kept the file in case a future iteration prefers to refactor with these
3 |
4 | const dockerApiHelper = {};
5 | const fetch = require('node-fetch');
6 | const dockerPort = 'http://localhost:2375';
7 |
8 | //all docker API calls will go here
9 |
10 | //fetch a list of running containers from the docker engine API
11 | dockerApiHelper.getContainerList = async () => {
12 | try {
13 | const response = await fetch(`${dockerPort}/containers/json?all=true&size=true`)
14 | const data = await response.json();
15 | return data;
16 | } catch (err) {
17 | console.log('error in get container list: ', err)
18 | }
19 | }
20 |
21 | dockerApiHelper.inspectContainer = (id) => {
22 | fetch(`${dockerPort}/containers/${id}/json?size=true`)
23 | .then(result => result.json())
24 | .then(data => {
25 | return data;
26 | })
27 | .catch(err => console.log('Error in docker helper inspect container: ', err))
28 | }
29 | const getDelta = (current, pre) => current - pre;
30 |
31 | dockerApiHelper.getStats = async (id) => {
32 | try {
33 | const response = await fetch(`${dockerPort}/containers/${id}/stats?stream=false`)
34 | const stats = await response.json();
35 | return stats;
36 |
37 | } catch (err) {
38 | console.log('Error in docker helper inspect container: ', err)
39 | }
40 | }
41 |
42 | dockerApiHelper.startContainer = (id) => {
43 | fetch(`${dockerPort}/containers/${id}/start`, {
44 | method: 'POST'
45 | })
46 | .then(result => result.json())
47 | .then(data => {
48 | return data;
49 | })
50 | .catch(err => console.log('Error in docker helper start container: ', err))
51 | }
52 |
53 | dockerApiHelper.stopContainer = (id) => {
54 | fetch(`${dockerPort}/containers/${id}/stop`, {
55 | method: 'POST'
56 | })
57 | .then(result => result.json())
58 | .then(data => {
59 | return data;
60 | })
61 | .catch(err => console.log('Error in docker helper stop container: ', err))
62 | }
63 |
64 | dockerApiHelper.restartContainer = (id) => {
65 | fetch(`${dockerPort}/containers/${id}/restart`, {
66 | method: 'POST'
67 | })
68 | .then(result => result.json())
69 | .then(data => {
70 | return data;
71 | })
72 | .catch(err => console.log('Error in docker helper restart container: ', err))
73 | }
74 |
75 | dockerApiHelper.removeContainer = (id) => {
76 | fetch(`${dockerPort}/containers/${id}`, {
77 | method: 'DELETE'
78 | })
79 | .then(result => result.json())
80 | .then(data => {
81 | return data;
82 | })
83 | .catch(err => console.log('Error in docker helper delete container: ', err))
84 | }
85 |
86 |
87 | module.exports = dockerApiHelper;
88 |
89 |
--------------------------------------------------------------------------------
/server/helpers/dockerCliHelper.js:
--------------------------------------------------------------------------------
1 | //these functions spawn a terminal to use the docker CLI
2 | const dockerCliHelper = {};
3 | const util = require('util');
4 | const exec = util.promisify(require('child_process').exec);
5 |
6 | //the output from docker CLI is not purely correct JSON, so this function takes in the output and creates an array of parsed objects
7 | const parseCliJSON = (stdout) => {
8 | const output = [];
9 | let dockerOutput = stdout.trim();
10 |
11 | const objs = dockerOutput.split('\n');
12 |
13 | for (let i = 0; i < objs.length; i++) {
14 | output.push(JSON.parse(objs[i]))
15 | }
16 | return output;
17 | }
18 |
19 | //here we get a list of all running or stopped containers
20 | dockerCliHelper.getContainerList = async () => {
21 | const { stdout, stderr } = await exec('docker ps --all --size --format "{{json .}}"')
22 | if (stderr) {
23 | console.log(stderr);
24 | return stderr;
25 | }
26 | const output = parseCliJSON(stdout)
27 | return output;
28 |
29 | }
30 |
31 | //here we can inspect a container - this is not currently in use
32 | dockerCliHelper.inspectContainer = async (id) => {
33 | const { stdout, stderr } = await exec(`docker inspect --format "{{json .}}" ${id}`)
34 |
35 | if (stderr) {
36 | console.log(stderr);
37 | return;
38 | }
39 | return parseCliJSON(stdout)
40 |
41 | }
42 |
43 | //here we get stats for a particular container by docker id
44 | dockerCliHelper.getStats = async (id) => {
45 | const { stdout, stderr } = await exec(`docker stats --no-stream --format "{{json .}}" ${id}`)
46 | if (stderr) {
47 | console.log(stderr);
48 | return;
49 | }
50 | return parseCliJSON(stdout);
51 |
52 | }
53 |
54 | //here we can start a container by docker id
55 | dockerCliHelper.startContainer = async (id) => {
56 | const { stdout, stderr } = await exec(`docker start ${id}`)
57 |
58 | if (stderr) {
59 | console.log(stderr);
60 | return;
61 | }
62 | return;
63 |
64 | }
65 |
66 | //here we can stop a container by docker id
67 | dockerCliHelper.stopContainer = async (id) => {
68 | const { stdout, stderr } = await exec(`docker stop ${id}`)
69 |
70 | if (stderr) {
71 | console.log(stderr);
72 | return;
73 | }
74 | return;
75 |
76 | }
77 |
78 | //here we can restart a container by docker id
79 | dockerCliHelper.restartContainer = async (id) => {
80 | const { stdout, stderr } = await exec(`docker restart ${id}`)
81 |
82 | if (stderr) {
83 | console.log(stderr);
84 | return;
85 | }
86 | return;
87 |
88 | }
89 |
90 | //here we can remove a container by docker id - not currently in use
91 | dockerCliHelper.removeContainer = async (id) => {
92 | const { stdout, stderr } = await exec(`docker rm ${id}`)
93 |
94 | if (stderr) {
95 | console.log(stderr);
96 | return;
97 | }
98 | return;
99 |
100 | }
101 |
102 |
103 |
104 | module.exports = { dockerCliHelper, parseCliJSON };
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const app = express();
4 | const { graphqlHTTP } = require('express-graphql');
5 | const PORT = 3000;
6 | const schema = require('./db/schema.js');
7 | const expressJwt = require('express-jwt');
8 | const { ApolloServer, gql } = require('apollo-server-express');
9 | const bodyParser = require('body-parser')
10 | const cookieParser = require('cookie-parser')
11 |
12 | app.use(express.json());
13 | app.use(express.static('build'));
14 | app.use(cookieParser());
15 | //use our graphql middleware. only to one endpoint
16 | app.use('/graphql', (req, res) => {
17 | return graphqlHTTP({
18 | schema,
19 | context: { req, res },
20 | graphiql: true
21 | })(req, res)
22 | }
23 | )
24 |
25 | //allow access to our index.html folder
26 | app.use('/', express.static(path.join(__dirname, '../client')));
27 |
28 | //serve the index file
29 | if (process.env.NODE_ENV === 'production') {
30 | //allow access to our index.html folder
31 | app.use('/build', express.static(path.join(__dirname, '../build')));
32 | app.get('/*', (req, res) => {
33 | res.sendFile(path.join(__dirname, '../client/index.html'));
34 | });
35 | }
36 |
37 | app.listen(PORT, () => {
38 | console.log(`Server listening on port ${PORT}`);
39 | })
40 |
41 | module.exports = app;
42 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin');
3 | const HtmlWebpackPlugin = require("html-webpack-plugin");
4 |
5 |
6 | module.exports = {
7 | entry: path.join(__dirname, './client/index.js'),
8 | devServer: {
9 | historyApiFallback: true,
10 | proxy: {
11 | '/graphql': {
12 | target: 'http://localhost:3000',
13 | secure: false,
14 | }
15 | }
16 | },
17 | plugins: [
18 | new NodePolyfillPlugin(),
19 | new HtmlWebpackPlugin({
20 | template: "./client/index.html",
21 | }),
22 | ],
23 | output: {
24 | path: path.resolve(__dirname, 'build'),
25 | filename: 'bundle.js',
26 | },
27 | mode: process.env.NODE_ENV,
28 | module: {
29 | rules: [
30 | {
31 | test: /\.tsx?$/,
32 | use: 'ts-loader',
33 | exclude: /node_modules/,
34 | },
35 | {
36 | test: /\.jsx?/,
37 | exclude: /node_modules/,
38 | use: {
39 | loader: 'babel-loader',
40 | options: {
41 | presets: ['@babel/preset-env', '@babel/preset-react']
42 | }
43 | }
44 | },
45 | {
46 | test: /\.(sa|sc|c)ss$/,
47 | // test: /\.css$/i,
48 | use: [
49 | 'style-loader',
50 | 'css-loader',
51 | 'postcss-loader',
52 | 'sass-loader',
53 | ],
54 | },
55 | {
56 | test: /\.(png|jpg|jpeg|gif)$/i,
57 | type: "asset/resource",
58 | },
59 | ]
60 | },
61 | // target: 'node',
62 | // externals: {
63 | // express: 'require(express)'
64 | // },
65 | resolve: {
66 | // Enable importing JS / JSX files without specifying their extension
67 | extensions: [".js", ".jsx"],
68 | },
69 | }
70 |
71 |
--------------------------------------------------------------------------------