├── .github ├── build.png ├── containers.png ├── dockureIconLogoV2.png ├── images.png ├── signin.png └── yamlEditor.png ├── .gitignore ├── README.md ├── babel.config.js ├── client ├── App.js ├── asset │ ├── ball.svg │ ├── dockureIcon.svg │ ├── dockureIconLogo.svg │ ├── dockureIconLogoV.svg │ ├── dockureIconLogoV2.svg │ ├── dockureIconLogoVW.svg │ ├── dockureIconLogoW.svg │ ├── dockureIconV.svg │ ├── dockureIconV2.svg │ ├── dockureIconV3.svg │ ├── dockureIconW1.svg │ ├── dockureIconW2.svg │ ├── dockureIconW3.svg │ ├── dockureLogo.svg │ ├── dockureLogoB.svg │ ├── dockureLogoTitle.svg │ └── dockureLogoW.svg ├── components │ ├── containerComponents │ │ ├── containerItem.js │ │ ├── containerItems.test.js │ │ ├── containerList.js │ │ └── dockerCommand.js │ ├── imageComponents │ │ ├── createImage.js │ │ ├── dockerBuild.js │ │ ├── editor.js │ │ ├── imageItem.js │ │ ├── imageItemDeleteBtn.js │ │ ├── imageList.js │ │ └── pullImage.js │ ├── loader.js │ ├── nav.js │ ├── selectors │ │ ├── stats.selector.js │ │ └── time.selector.js │ ├── statComponents │ │ ├── graph.js │ │ └── statsContainer.js │ ├── titlebar.js │ └── userComponents │ │ ├── login.js │ │ ├── signUp.js │ │ └── userStatus.js ├── containers │ ├── contentContainer.js │ ├── imageContainer.js │ ├── mainContainer.js │ ├── protectedRoute.js │ └── unProtectedRoute.js ├── db │ ├── token.js │ └── user.js ├── index.js ├── listButton.js ├── redux │ ├── action │ │ ├── action.js │ │ └── actionTypes.js │ └── reducers │ │ ├── containerReducer.js │ │ └── index.js ├── scss │ ├── application.scss │ ├── containerList.scss │ ├── createImage.scss │ ├── dockerCommand.scss │ ├── images.scss │ ├── login.scss │ ├── mainContainer.scss │ ├── nav.scss │ ├── stats.scss │ ├── titlebar.scss │ ├── userStatus.scss │ └── variables.scss ├── services │ ├── axiosService.js │ ├── containerService.js │ ├── containerService.test.js │ ├── imageService.js │ ├── prometheusService.js │ ├── userDbService.js │ └── utilities.js └── store.js ├── electron └── main.js ├── index.html ├── package.json ├── server ├── assets │ └── prometheus.yaml ├── config.js ├── controllers │ ├── authentication.js │ ├── cadvisorStart.js │ ├── dContainer.js │ ├── dImage.js │ ├── isAuth.js │ ├── metricQueries.js │ ├── nodeExporter.js │ ├── promMetrics.js │ ├── timeConversion.js │ ├── userController.js │ └── ymlParser.js ├── database │ ├── dbConnect.js │ └── setup.sql ├── routers │ ├── dContainer.js │ ├── dImage.js │ ├── promMetrics.js │ └── user.js └── server.js ├── tsconfig.json ├── webpack.config.js ├── yarn-error.log └── yarn.lock /.github/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/dockure/a7bee2cd86829dada9abd51789d066904ce46f57/.github/build.png -------------------------------------------------------------------------------- /.github/containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/dockure/a7bee2cd86829dada9abd51789d066904ce46f57/.github/containers.png -------------------------------------------------------------------------------- /.github/dockureIconLogoV2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/dockure/a7bee2cd86829dada9abd51789d066904ce46f57/.github/dockureIconLogoV2.png -------------------------------------------------------------------------------- /.github/images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/dockure/a7bee2cd86829dada9abd51789d066904ce46f57/.github/images.png -------------------------------------------------------------------------------- /.github/signin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/dockure/a7bee2cd86829dada9abd51789d066904ce46f57/.github/signin.png -------------------------------------------------------------------------------- /.github/yamlEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/dockure/a7bee2cd86829dada9abd51789d066904ce46f57/.github/yamlEditor.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | package-lock.json 4 | .env 5 | promConfigFile.yaml 6 | data-out.yaml 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Contributors][contributors-shield]][contributors-url] 2 | [![Forks][forks-shield]][forks-url] 3 | [![Stargazers][stars-shield]][stars-url] 4 | [![Issues][issues-shield]][issues-url] 5 | 6 | 8 | 9 | 10 | 11 | 12 |
13 |

14 | 15 | Logo 16 | 17 |

18 | Simplifying the containerization process outside of the command line. 19 |
20 | Explore the docs » 21 |
22 |
23 | View Demo 24 | · 25 | Report Bug 26 | · 27 | Request Feature 28 |

29 |

30 | 31 | 32 | 33 | 34 |
35 | Table of Contents 36 |
    37 |
  1. 38 | About The Project 39 | 42 |
  2. 43 |
  3. 44 | Getting Started 45 | 49 |
  4. 50 |
  5. Usage
  6. 51 |
  7. Roadmap
  8. 52 |
  9. Contributing
  10. 53 |
  11. License
  12. 54 |
  13. Contact
  14. 55 |
  15. Acknowledgements
  16. 56 |
57 |
58 | 59 | 60 | 61 | 62 | ## About The Project 63 | 64 | ### Built With 65 | * [React](https://reactjs.org/docs/getting-started.html) 66 | * [Redux](https://redux.js.org/) 67 | * [Express](https://expressjs.com/) 68 | * [Docker](https://docs.docker.com/) 69 | * [Docker REST API](https://docs.docker.com/engine/api/v1.41/#) 70 | * [Node child_process](https://nodejs.org/api/child_process.html) 71 | * [Electron](https://www.electronjs.org/docs) 72 | * [Prometheus](https://prometheus.io/docs/introduction/overview/) 73 | * [CAdvisor](https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md) 74 | * [Socat (in the localhost)]() 75 | 76 | 77 | ## Getting Started 78 | 79 | Starting our app is super easy! Just make sure you have Yarn and Docker if you don't already. 80 | 81 | ### Prerequisites 82 | In order for the application to work: 83 | 1. Must have [Docker Desktop](https://www.docker.com/products/docker-desktop) or Docker Daemon running in the background. 84 | 2. Must install yarn: 85 | ```sh 86 | npm install yarn 87 | ``` 88 | * For security purposes, please make sure you use this app in your local network unless you provide your own TCP TLS/SSH security. Pre-packaged SSH capabilities are currently in beta! 89 | ### Installation 90 | The pre-bundled app will be coming in Dockure 2.0, but for now: 91 | 1. Clone the repo 92 | 2. install dependencies 93 | ```sh 94 | yarn install 95 | ``` 96 | 3. create an .env file in the root directory filling in your personal DB_host and JWT secret like below: 97 | ``` 98 | # Database 99 | DB_HOST=your.db.here 100 | # Bcrypt 101 | BCRYPT_SALT_ROUNDS=10 102 | # JWT 103 | JWT_SECRET=your.secret.here 104 | JWT_EXPIRES_SEC=86400 105 | ``` 106 | ``` 107 | |-- .github (folder) 108 | |-- client (folder) 109 | |-- electron (folder) 110 | |-- server (folder) 111 | 112 | |-- .env (file) <----- right here! 113 | |-- index.html 114 | |-- etcetra... 115 | ``` 116 | 4. build the app 117 | ```sh 118 | yarn build 119 | ``` 120 | 5. start it 121 | ```sh 122 | yarn start 123 | ``` 124 | * After logging in for the first time, it may take some time for dependencies to load. 125 | 126 | ![signin](https://raw.githubusercontent.com/oslabs-beta/dockure/dev/.github/signin.png) 127 | 128 | 129 | 130 | Alternatively, you can skip steps 4-5 and run this application in dev mode outside of electron: 131 | ```sh 132 | yarn dev 133 | ``` 134 | 135 | 136 | 137 | ## Usage 138 | Once you are logged in there are loads you can do. Here are some examples: 139 | 140 | * Our simple homepage displays containers and their data. You can view container data and/or select multiple containers you'd like to start and stop 141 | ![containers](https://raw.githubusercontent.com/oslabs-beta/dockure/dev/.github/containers.png) 142 | 143 | * In our images tab, you can run your images to build containers. You can also pull locally or on Docker Hub and build images. 144 | ![imagestab](https://raw.githubusercontent.com/oslabs-beta/dockure/dev/.github/images.png) 145 | 146 | ![build](https://raw.githubusercontent.com/oslabs-beta/dockure/dev/.github/build.png) 147 | 148 | 149 | * In our YAML/Dockerfile editor tab, we provide a simple Dockerfile or YAML editor for you to create, edit and save your own files without opening up an IDE. 150 | ![yaml](https://raw.githubusercontent.com/oslabs-beta/dockure/dev/.github/yamlEditor.png) 151 | 152 | 153 | ## Roadmap 154 | 155 | See the [open issues](https://github.com/oslabs-beta/dockure/issues) for a list of proposed features (and known issues). 156 | 157 | 158 | 159 | 160 | ## Contributing 161 | 162 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 163 | 164 | 1. Fork the Project 165 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 166 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 167 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 168 | 5. Open a Pull Request 169 | 170 | 171 | ## License 172 | 173 | Distributed under the MIT License. See `LICENSE` for more information. 174 | 175 | 176 | ## Contact 177 | 178 | 179 | * Liam - [LinkedIn](https://www.linkedin.com/in/liamtalty/), [Github](https://github.com/lptalty) 180 | * Van - [LinkedIn](https://www.linkedin.com/in/van-biet-nguyen/), [Github](https://github.com/vanbietnguyen) 181 | * Alex - [LinkedIn](https://www.linkedin.com/in/alexander-zayas-jr/), [Github](https://github.com/AlexZayas) 182 | * Hazel - [LinkedIn](https://www.linkedin.com/in/hyeseon-na/), [Github](https://github.com/hazel0109) 183 | * Nate - [LinkedIn](https://Linkedin.com/in/nathanael-tracy/), [Github](https://github.com/n-tracy1) 184 | 185 | 186 | ## Acknowledgements 187 | * [Yarn](https://classic.yarnpkg.com/en/docs/) 188 | * [Webpack](https://webpack.js.org/) 189 | * [CodeMirror](https://codemirror.net/doc/manual.html) 190 | * [BCrypt](https://www.npmjs.com/package/bcrypt) 191 | * [PostGreSQL](https://www.postgresql.org/docs/) 192 | 193 | 194 | 195 | [contributors-shield]: https://img.shields.io/github/contributors/oslabs-beta/dockure.svg?style=for-the-badge 196 | [contributors-url]: https://github.com/oslabs-beta/dockure/graphs/contributors 197 | [forks-shield]: https://img.shields.io/github/forks/oslabs-beta/dockure.svg?style=for-the-badge 198 | [forks-url]: https://github.com/oslabs-beta/dockure/network/members 199 | [stars-shield]: https://img.shields.io/github/stars/oslabs-beta/dockure.svg?style=for-the-badge 200 | [stars-url]: https://github.com/oslabs-beta/dockure/stargazers 201 | [issues-shield]: https://img.shields.io/github/issues/oslabs-beta/dockure.svg?style=for-the-badge 202 | [issues-url]: https://github.com/oslabs-beta/dockure/issues 203 | 204 | 206 | 209 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['@babel/plugin-transform-async-to-generator'], 3 | presets: [ 4 | '@babel/preset-env', 5 | '@babel/preset-react', 6 | '@babel/preset-typescript', 7 | ], 8 | env: { 9 | test: { 10 | plugins: ['@babel/plugin-transform-runtime'], 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /client/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainContainer from './containers/mainContainer'; 3 | import Login from './components/userComponents/login'; 4 | import SignUP from './components/userComponents/signUp'; 5 | import { HashRouter as Router, Switch } from 'react-router-dom'; 6 | import ProtectedRoute from './containers/protectedRoute'; 7 | import UnProtectedRoute from './containers/unProtectedRoute'; 8 | 9 | const App = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /client/asset/ball.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/asset/dockureIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /client/asset/dockureIconLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 80 | 81 | 82 | 83 | 84 | 85 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 99 | 102 | 104 | 110 | 116 | 122 | 126 | 132 | 137 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /client/asset/dockureIconLogoV.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 97 | 100 | 102 | 108 | 113 | 119 | 123 | 129 | 134 | 140 | 141 | -------------------------------------------------------------------------------- /client/asset/dockureIconLogoV2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 97 | 100 | 102 | 108 | 113 | 119 | 123 | 129 | 134 | 140 | 141 | -------------------------------------------------------------------------------- /client/asset/dockureIconLogoVW.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 97 | 100 | 102 | 108 | 113 | 119 | 123 | 129 | 134 | 140 | 141 | -------------------------------------------------------------------------------- /client/asset/dockureIconV.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /client/asset/dockureIconV2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /client/asset/dockureIconV3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /client/asset/dockureIconW1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /client/asset/dockureIconW2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /client/asset/dockureIconW3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 71 | 72 | 73 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /client/asset/dockureLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 14 | 17 | 20 | 22 | 28 | 33 | 39 | 43 | 48 | 52 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /client/asset/dockureLogoB.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 14 | 17 | 20 | 22 | 28 | 34 | 40 | 44 | 50 | 55 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /client/asset/dockureLogoTitle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 14 | 17 | 20 | 22 | 28 | 33 | 39 | 43 | 48 | 52 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /client/asset/dockureLogoW.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 14 | 17 | 20 | 22 | 28 | 33 | 39 | 43 | 48 | 52 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /client/components/containerComponents/containerItem.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import moment from 'moment'; 3 | 4 | const ContainerItem = ({ 5 | container, 6 | getData, 7 | onCheckboxClickCallback, 8 | isChecked, 9 | }) => { 10 | const [defaultCon, setDefaultCon] = useState(false); 11 | const utc = new Date(0); 12 | const date = utc.setUTCSeconds(container.Created); 13 | 14 | useEffect(() => { 15 | setDefaultCon(() => { 16 | const name = container.Names[0].slice(1); 17 | if (name === 'cadvisor' || name === 'prometheus' || name === 'socat') { 18 | return true; 19 | } 20 | }); 21 | }, [container]); 22 | 23 | return ( 24 |
  • 25 |
    26 |
    27 | onCheckboxClickCallback(e.target.value)} 33 | /> 34 |
    {container.Names[0].slice(1)}
    35 |
    36 |
    {moment(date).fromNow()}
    37 |
    38 |
    39 |
    40 | {container.State} 41 |
    42 | 45 |
    46 |
  • 47 | ); 48 | }; 49 | 50 | export default ContainerItem; 51 | 52 | function containerStatus(state) { 53 | switch (state) { 54 | case 'running': 55 | return 'is_running'; 56 | case 'exited': 57 | return 'is_exited'; 58 | default: 59 | return 'is_else'; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /client/components/containerComponents/containerItems.test.js: -------------------------------------------------------------------------------- 1 | import { render, fireEvent, waitFor, screen } from '@testing-library/react'; 2 | import React from 'react'; 3 | import '@testing-library/jest-dom'; 4 | import ContainerItem from './containerItem'; 5 | 6 | describe('Container Items', () => { 7 | const props = { 8 | id: 1001, 9 | container: { 10 | Id: 1001, 11 | Names: ['0test'], 12 | State: 'running', 13 | Created: 1367854155, 14 | }, 15 | getData: () => {}, 16 | onCheckboxClickCallback: () => {}, 17 | conStatus: true, 18 | isChecked: false, 19 | }; 20 | 21 | describe('render the component', () => { 22 | it('should have a button', () => { 23 | render(); 24 | const button = screen.getByText(/get data/i); 25 | expect(button).toBeInTheDocument(); 26 | }); 27 | it('should render the container name only the first word', () => { 28 | render(); 29 | const name = screen.getByText('test'); 30 | expect(name).toBeInTheDocument(); 31 | }); 32 | it('should render how long is the container since it is created', () => { 33 | render(); 34 | const time = screen.getByText(/ago/i); 35 | expect(time).toBeInTheDocument(); 36 | }); 37 | }); 38 | 39 | describe('assign class name based on the container state', () => { 40 | it('should have class name is_running', () => { 41 | render(); 42 | const state = screen.getByText('running'); 43 | expect(state).toHaveClass('is_running'); 44 | }); 45 | it('should have class name is_existed', () => { 46 | const props2 = { 47 | ...props, 48 | container: { 49 | ...props.container, 50 | State: 'exited', 51 | }, 52 | }; 53 | render(); 54 | const state = screen.getByText('exited'); 55 | expect(state).toHaveClass('is_exited'); 56 | }); 57 | it('should have class name is_else', () => { 58 | const props3 = { 59 | ...props, 60 | container: { 61 | ...props.container, 62 | State: 'created', 63 | }, 64 | }; 65 | render(); 66 | const state = screen.getByText('created'); 67 | expect(state).toHaveClass('is_else'); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /client/components/containerComponents/containerList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ContainerItem from './containerItem'; 3 | import containerService from '../../services/containerService'; 4 | import { useDispatch } from 'react-redux'; 5 | import { setStateMetrics } from '../../redux/action/action.js'; 6 | import { useSelector } from 'react-redux'; 7 | import { timeSelector } from '../selectors/time.selector'; 8 | import Loader from '../loader'; 9 | 10 | const ContainerList = ({ conList, onCheckboxClickCallback, selectedIds }) => { 11 | const { time } = useSelector(timeSelector); 12 | const dispatch = useDispatch(); 13 | const getData = async (id, containerState) => { 14 | let stats = { 15 | cpu: [], 16 | memory: [], 17 | }; 18 | if (containerState === 'running') 19 | stats = await containerService.getMetrics(id, time); 20 | dispatch(setStateMetrics(stats)); 21 | }; 22 | 23 | if (!conList) return null; 24 | 25 | if (!conList.length) return ; 26 | 27 | const con = conList.map((container, inx) => { 28 | const isChecked = !!selectedIds[container.Id]; 29 | return ( 30 | getData(container.Id, container.State)} 34 | container={container} 35 | isChecked={isChecked} 36 | /> 37 | ); 38 | }); 39 | 40 | return
      {con}
    ; 41 | }; 42 | 43 | export default ContainerList; 44 | -------------------------------------------------------------------------------- /client/components/containerComponents/dockerCommand.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import ContainerList from './containerList'; 3 | import { Link } from 'react-router-dom'; 4 | import ContainerService from '../../services/containerService'; 5 | import { throttle } from '../../services/utilities'; 6 | 7 | const DockerCommand = ({ 8 | conList, 9 | toggle, 10 | callConStatus, 11 | loading, 12 | setLoading, 13 | error, 14 | }) => { 15 | const [selectedIds, setSelectedIds] = useState({}); 16 | const onCheckboxClickCallback = (id) => { 17 | let newSelectedIds = { ...selectedIds }; 18 | if (selectedIds[id]) { 19 | delete newSelectedIds[id]; 20 | setSelectedIds(newSelectedIds); 21 | return; 22 | } 23 | newSelectedIds[id] = true; 24 | setSelectedIds(newSelectedIds); 25 | }; 26 | 27 | const updateContainerStatuses = (ids, command) => { 28 | setLoading(true); 29 | const promiseArr = []; 30 | for (let id in ids) { 31 | let result = ContainerService.postClickBtn(command, id); 32 | promiseArr.push(result); 33 | } 34 | Promise.all(promiseArr).then((values) => { 35 | setSelectedIds({}); 36 | callConStatus(); 37 | }); 38 | }; 39 | 40 | const updateContainerStatusesThrottle = useCallback( 41 | throttle(updateContainerStatuses, 5000), 42 | [] 43 | ); 44 | 45 | const onClickButton = (command) => { 46 | updateContainerStatusesThrottle(selectedIds, command); 47 | }; 48 | 49 | return ( 50 |
    55 |
    56 |
      57 | 63 | 69 | 75 | 81 | 87 | 93 | 99 |
    100 | 101 | 104 | 105 |
    106 | {error && ( 107 |
    Cannot get the docker containers. Please reopen the app
    108 | )} 109 | {loading &&
    Please wait...
    } 110 | 111 | 116 |
    117 | ); 118 | }; 119 | export default DockerCommand; 120 | -------------------------------------------------------------------------------- /client/components/imageComponents/createImage.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Editor from './editor.js'; 3 | import imageService from '../../services/imageService'; 4 | 5 | const CreateImage = ({ toggle }) => { 6 | const [yaml, setYaml] = useState(imageService.yamlBoiler()); 7 | const [dockerfile, setDockerfile] = useState(imageService.dockerBoiler()); 8 | 9 | return ( 10 |
    15 | 22 | 23 | 30 |
    31 | ); 32 | }; 33 | 34 | export default CreateImage; 35 | -------------------------------------------------------------------------------- /client/components/imageComponents/dockerBuild.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import imageService from '../../services/imageService'; 3 | 4 | const DockerBuild = ({ updateImage, setUpdateImage }) => { 5 | const defaultPath = '~/'; 6 | const [dockerPath, setDockerPath] = useState(defaultPath); 7 | const [imageName, setImageName] = useState(''); 8 | 9 | const handleBuild = async (e) => { 10 | e.preventDefault(); 11 | let result = await imageService.buildImage({ 12 | imageName: imageName, 13 | path: dockerPath, 14 | }); 15 | setUpdateImage(!updateImage); 16 | }; 17 | 18 | return ( 19 |
    20 | setImageName(e.target.value)} 26 | /> 27 | setDockerPath(e.target.value)} 32 | /> 33 | 34 | 35 |
    36 | ); 37 | }; 38 | 39 | export default DockerBuild; 40 | -------------------------------------------------------------------------------- /client/components/imageComponents/editor.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import CodeMirror from 'codemirror/lib/codemirror.js'; 3 | import 'codemirror/mode/dockerfile/dockerfile'; 4 | import 'codemirror/mode/yaml/yaml'; 5 | import 'codemirror/lib/codemirror.css'; 6 | import 'codemirror/theme/nord.css'; 7 | import { Controlled as ControlledEditor } from 'react-codemirror2'; 8 | import { saveAs } from 'file-saver'; 9 | 10 | const Editor = (props) => { 11 | const { language, displayName, value, onChange, saveType } = props; 12 | 13 | const [open, setOpen] = useState(true); 14 | 15 | const handleChange = (editor, data, value) => { 16 | onChange(value); 17 | }; 18 | 19 | const save = async () => { 20 | const file = await new Blob([value], { type: `text/${displayName}` }); 21 | saveAs(file, `${saveType}`); 22 | }; 23 | 24 | return ( 25 | <> 26 |
    27 |
    28 | {' '} 29 | {`Edit your ${displayName}`} 30 | 36 |
    37 |
    38 | 50 |
    51 |
    52 | {`Save your ${displayName}`} 53 |
    54 |
    55 | 56 | ); 57 | }; 58 | 59 | export default Editor; 60 | -------------------------------------------------------------------------------- /client/components/imageComponents/imageItem.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import imageService from '../../services/imageService'; 3 | import { useHistory } from 'react-router-dom'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'; 6 | import ImageItemDeleteBtn from './imageItemDeleteBtn'; 7 | 8 | const ImageItem = ({ image, setUpdateImage, updateImage }) => { 9 | const [isRunning, setIsRunning] = useState(); 10 | const [optClick, setOptClick] = useState(false); 11 | let history = useHistory(); 12 | 13 | const checkRepoTag = ({ image }) => { 14 | if (image.RepoTags) { 15 | if (image.RepoTags[0] === ':') { 16 | image.RepoTags = ['Anonymous']; 17 | } 18 | return image.RepoTags[0]; 19 | } else if (image.RepoDigests) { 20 | const nameArr = image.RepoDigests[0].split('@'); 21 | return nameArr[0]; 22 | } else { 23 | return 'Anonymous'; 24 | } 25 | }; 26 | 27 | const startClick = async (e) => { 28 | const ID = { image }.image.Id.slice(7, 19); 29 | const handleSubmit = await imageService.startImage(ID); 30 | if (handleSubmit.data === 'running') { 31 | setIsRunning(true); 32 | history.push('/main'); 33 | } 34 | }; 35 | 36 | const deleteClick = async (e) => { 37 | const ID = { image }.image.Id.slice(7, 19); 38 | setOptClick(false); 39 | await imageService.deleteImage(ID); 40 | setUpdateImage(!updateImage); 41 | }; 42 | 43 | const optHandler = () => { 44 | setOptClick(!optClick); 45 | }; 46 | 47 | return ( 48 |
    49 |
    50 |
    51 |
    Image Name
    52 |
    53 | 54 |
    55 | {optClick && } 56 |
    57 |
    {checkRepoTag({ image })}
    58 |
    59 | {isRunning ? ( 60 |
    In Use
    61 | ) : ( 62 | 65 | )} 66 |
    67 |
    68 | ); 69 | }; 70 | 71 | export default ImageItem; 72 | -------------------------------------------------------------------------------- /client/components/imageComponents/imageItemDeleteBtn.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ImageItemDeleteBtn = ({ deleteClick }) => { 4 | return ( 5 |
    6 | 9 |
    10 | ); 11 | }; 12 | export default ImageItemDeleteBtn; 13 | -------------------------------------------------------------------------------- /client/components/imageComponents/imageList.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ImageItem from './imageItem'; 3 | import PullImage from './pullImage'; 4 | import DockerBuild from './dockerBuild'; 5 | import Loader from '../loader'; 6 | 7 | const ImageList = ({ imageList, updateImage, setUpdateImage }) => { 8 | const [dockerAction, setDockerAction] = useState(true); 9 | 10 | const handleChange = (e) => { 11 | if (e.target.value === 'Build') return setDockerAction(false); 12 | else return setDockerAction(true); 13 | }; 14 | 15 | if (!imageList) return null; 16 | 17 | if (!imageList.length) return ; 18 | 19 | const image = imageList.map((image, inx) => { 20 | return ( 21 | 28 | ); 29 | }); 30 | 31 | return ( 32 |
    33 |
    34 | 38 | {dockerAction ? ( 39 | 43 | ) : ( 44 | 48 | )} 49 |
    50 |
      {image}
    51 |
    52 | ); 53 | }; 54 | 55 | export default ImageList; 56 | -------------------------------------------------------------------------------- /client/components/imageComponents/pullImage.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import imageService from '../../services/imageService'; 3 | 4 | const PullImage = ({ updateImage, setUpdateImage }) => { 5 | const [imageName, setImageName] = useState(''); 6 | 7 | const handlePull = async () => { 8 | await imageService.pullImageInfo(imageName); 9 | setUpdateImage(!updateImage); 10 | }; 11 | 12 | const onSubmit = () => { 13 | const input = document.querySelector('.image_input'); 14 | setImageName(''); 15 | handlePull(); 16 | input.focus(); 17 | }; 18 | 19 | const searchSubmit = () => { 20 | window.open(`https://hub.docker.com/search?q=${imageName}&type=image`); 21 | }; 22 | 23 | return ( 24 |
    25 | setImageName(e.target.value)} 31 | /> 32 | 35 |

    or

    36 | 39 |
    40 | ); 41 | }; 42 | 43 | export default PullImage; 44 | -------------------------------------------------------------------------------- /client/components/loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ball from '../asset/ball.svg'; 3 | 4 | const Loader = () => { 5 | return ; 6 | }; 7 | 8 | export default Loader; 9 | -------------------------------------------------------------------------------- /client/components/nav.js: -------------------------------------------------------------------------------- 1 | import React, { component } from 'react'; 2 | import dockureIconV2 from '../asset/dockureIconLogoV2.svg'; 3 | 4 | import { 5 | BrowserRouter as Router, 6 | Switch, 7 | Route, 8 | useRouteMatch, 9 | Link, 10 | Redirect, 11 | } from 'react-router-dom'; 12 | 13 | const Nav = () => { 14 | let main = useRouteMatch(); 15 | return ( 16 |
      17 | 18 | {/* */} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
    29 | ); 30 | }; 31 | 32 | export default Nav; 33 | -------------------------------------------------------------------------------- /client/components/selectors/stats.selector.js: -------------------------------------------------------------------------------- 1 | export const getMetricsSelector = (state) => ({ 2 | metrics: state.containers.metrics, 3 | }) -------------------------------------------------------------------------------- /client/components/selectors/time.selector.js: -------------------------------------------------------------------------------- 1 | export const timeSelector = (state) => ({ 2 | time: state.containers.time, 3 | }) -------------------------------------------------------------------------------- /client/components/statComponents/graph.js: -------------------------------------------------------------------------------- 1 | import React, { component, useEffect, useState } from 'react'; 2 | import { 3 | LineChart, 4 | Line, 5 | AreaChart, 6 | Area, 7 | CartesianGrid, 8 | XAxis, 9 | YAxis, 10 | Tooltip, 11 | ResponsiveContainer, 12 | BarChart, 13 | Bar, 14 | } from 'recharts'; 15 | 16 | const Graph = ({ data, dataKey, dataType }) => { 17 | const CustomTooltip = ({ active, payload, label }) => { 18 | if (active) { 19 | let value; 20 | if (payload === null) return null; 21 | value = Object.values(payload[0].payload); 22 | 23 | return ( 24 |
    34 |

    {`Time: ${label} value: ${ 35 | value === undefined ? 0 : value[1].toFixed(5) 36 | }`}

    37 |
    38 | ); 39 | } else return
    loading
    ; 40 | }; 41 | 42 | return ( 43 |
    44 |
    45 | 46 | {/* */} 47 | 48 | 49 | 50 | 51 | 52 | {/* */} 53 | 54 | 55 | 61 | `${number.toFixed(2)}%`} 65 | dataKey={dataKey} 66 | domain={['auto', 'auto']} 67 | style={{ fontSize: '.8rem' }} 68 | label={{ 69 | value: `Percent of Total ${dataType} Used`, 70 | angle: -90, 71 | position: 'insideLeft', 72 | dy: 100, 73 | dx: -3, 74 | style: { 75 | fill: '#989898', 76 | fontSize: '1rem', 77 | }, 78 | }} 79 | /> 80 | } /> 81 | 82 | 88 | 89 |
    90 |
    91 | ); 92 | }; 93 | 94 | export default Graph; 95 | -------------------------------------------------------------------------------- /client/components/statComponents/statsContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Graph from './graph'; 3 | 4 | import { useDispatch } from 'react-redux'; 5 | import { setTimeSelector } from '../../redux/action/action.js'; 6 | import { useSelector } from 'react-redux'; 7 | import { getMetricsSelector } from '../selectors/stats.selector'; 8 | 9 | const Stats = () => { 10 | const { metrics } = useSelector(getMetricsSelector); 11 | const dispatch = useDispatch(); 12 | 13 | return ( 14 |
    15 |
    16 | 24 | 30 | 36 |
    37 | 38 |
    CPU Usage
    39 | 44 |
    Memory Usage
    45 | 50 |
    51 | ); 52 | }; 53 | 54 | export default Stats; 55 | -------------------------------------------------------------------------------- /client/components/titlebar.js: -------------------------------------------------------------------------------- 1 | import React, { component, useEffect, useState } from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import UserStatus from './userComponents/userStatus'; 4 | import dockureLogoTitle from '../asset/dockureLogoTitle.svg'; 5 | import dockureIconW from '../asset/dockureIconW1.svg'; 6 | import { faBars, faUser } from '@fortawesome/free-solid-svg-icons'; 7 | 8 | const Titlebar = ({ toggle, setToggle, isLogin, setIsLogin, userName }) => { 9 | const [userStat, setUserStat] = useState(false); 10 | 11 | const toggleHandler = () => { 12 | setToggle(!toggle); 13 | }; 14 | 15 | const userHandler = () => { 16 | setUserStat(!userStat); 17 | }; 18 | 19 | useEffect(() => { 20 | const offUserStat = () => { 21 | setUserStat(false); 22 | }; 23 | if (userStat) { 24 | document.body.addEventListener('click', offUserStat); 25 | } 26 | return () => { 27 | document.body.removeEventListener('click', offUserStat); 28 | }; 29 | }, [userStat]); 30 | 31 | return ( 32 | <> 33 | {isLogin ? ( 34 |
    35 |
    36 | {/*
    */} 37 |
    38 | 39 |
    40 | {/*
    */} 41 | 42 |
    43 | 44 | 45 | 46 |
    47 | 48 |
    49 |
    50 |
    51 |
    52 | ) : ( 53 |
    54 | 55 |
    56 | )} 57 | {userStat && ( 58 | 63 | )} 64 | 65 | ); 66 | }; 67 | 68 | export default Titlebar; 69 | -------------------------------------------------------------------------------- /client/components/userComponents/login.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link, Redirect } from 'react-router-dom'; 3 | import Titlebar from '../titlebar'; 4 | import UserDbService from '../../services/userDbService'; 5 | import TokenStorage from '../../db/token'; 6 | 7 | const Login = () => { 8 | const [userData, setUserData] = useState({ username: '', password: '' }); 9 | const [showError, setShowError] = useState(false); 10 | const [isAuthenticated, setIsAuthenticated] = useState(false); 11 | 12 | const userHandler = (e) => { 13 | setUserData((userData) => ({ 14 | ...userData, 15 | [e.target.name]: e.target.value, 16 | })); 17 | }; 18 | 19 | const postSignIn = async () => { 20 | const pwInput = document.querySelector('.signin_pw'); 21 | const result = await UserDbService.postUserData(userData); 22 | if (result.id) { 23 | TokenStorage.saveToken(result.token); 24 | return setIsAuthenticated(true); 25 | } 26 | setUserData((userData) => ({ 27 | ...userData, 28 | password: '', 29 | })); 30 | pwInput.focus(); 31 | return setShowError(true); 32 | }; 33 | 34 | const userSignIn = (e) => { 35 | e.preventDefault(); 36 | if (!userData.username || !userData.password) { 37 | return setShowError(true); 38 | } 39 | postSignIn(); 40 | }; 41 | 42 | if (isAuthenticated) { 43 | return ; 44 | } 45 | 46 | return ( 47 |
    48 | 49 |
    50 |
    51 |

    Welcome back!

    52 |

    You are almost in the promise land

    53 | 54 | 55 | 56 |
    57 |
    58 |

    Sign in to Dockure

    59 |
    60 | 68 | 76 | {showError && ( 77 |
    Invalid username or password
    78 | )} 79 | 82 |
    83 |
    84 |
    85 |
    86 | ); 87 | }; 88 | 89 | export default Login; 90 | -------------------------------------------------------------------------------- /client/components/userComponents/signUp.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Titlebar from '../titlebar'; 3 | import { Link, Redirect } from 'react-router-dom'; 4 | import UserDbService from '../../services/userDbService'; 5 | 6 | const SignUP = () => { 7 | const [userData, setUserData] = useState({ 8 | username: '', 9 | email: '', 10 | password: '', 11 | }); 12 | const [showUserError, setShowUserError] = useState(false); 13 | const [showPWError, setShowPWError] = useState(false); 14 | const [isSignUp, setIsSignUp] = useState(false); 15 | 16 | const userHandler = (e) => { 17 | setUserData((userData) => ({ 18 | ...userData, 19 | [e.target.name]: e.target.value, 20 | })); 21 | }; 22 | 23 | const sendUserData = async () => { 24 | const result = await UserDbService.postUserData( 25 | 'http://localhost:3000/api/user/signup', 26 | userData 27 | ); 28 | }; 29 | 30 | const signUpHandler = (e) => { 31 | if (!userData.username || !userData.email) { 32 | setShowUserError(true); 33 | setShowPWError(false); 34 | return; 35 | } 36 | if (userData.password.length < 5) { 37 | setShowUserError(false); 38 | setShowPWError(true); 39 | return; 40 | } 41 | sendUserData(); 42 | setIsSignUp(true); 43 | }; 44 | 45 | if (isSignUp) { 46 | return ; 47 | } 48 | 49 | //*******only when the sign up is successful, we are going to sign in link.****** 50 | 51 | return ( 52 |
    53 | 54 |
    55 |
    56 |

    Create Account

    57 |
    58 | 66 | 74 | 82 | {showUserError && ( 83 |
    84 | Username and Email cannot be empty 85 |
    86 | )} 87 | {showPWError && ( 88 |
    89 | Password must be 5 characters or more 90 |
    91 | )} 92 |
    93 | 96 |
    97 |
    98 |

    Hello, Friend!

    99 |

    Enter your personal details

    100 |

    and enjoy Dockure

    101 | 102 | 103 | 104 |
    105 |
    106 |
    107 | ); 108 | }; 109 | 110 | export default SignUP; 111 | -------------------------------------------------------------------------------- /client/components/userComponents/userStatus.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Link } from 'react-router-dom'; 3 | import UserDbService from '../../services/userDbService'; 4 | 5 | const UserStatus = ({ userHandler, userName, setIsLogin }) => { 6 | const signOutHandler = () => { 7 | UserDbService.logout(); 8 | setIsLogin(false); 9 | userHandler(); 10 | }; 11 | 12 | return ( 13 |
    14 |
    15 |
    Hello, {userName}
    16 |
    17 |
    18 | 19 | 22 | 23 |
    24 |
    25 | ); 26 | }; 27 | 28 | export default UserStatus; 29 | -------------------------------------------------------------------------------- /client/containers/contentContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react'; 2 | import DockerCommand from '../components/containerComponents/dockerCommand'; 3 | import StatsContainer from '../components/statComponents/statsContainer'; 4 | import ContainerService from '../services/containerService'; 5 | 6 | const ContentContainer = ({ toggle }) => { 7 | const [conList, setConList] = useState([]); 8 | const [error, setError] = useState(null); 9 | const [loading, setLoading] = useState(false); 10 | 11 | useEffect(() => { 12 | const setupCon = async () => { 13 | try { 14 | await ContainerService.setupCon(); 15 | const clear = setTimeout(() => callConStatus(), 1000); 16 | return () => clearTimeout(clear); 17 | } catch (err) { 18 | setError(true); 19 | } 20 | }; 21 | setupCon(); 22 | }, []); 23 | 24 | const callConStatus = useCallback(async () => { 25 | try { 26 | const result = await ContainerService.getConInfo(); 27 | setConList(result); 28 | setLoading(false); 29 | } catch (err) { 30 | setError(true); 31 | } 32 | }, []); 33 | 34 | return ( 35 |
    36 | 44 | 45 |
    46 | ); 47 | }; 48 | 49 | export default ContentContainer; 50 | -------------------------------------------------------------------------------- /client/containers/imageContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import imageService from '../services/imageService'; 3 | import ImageList from '../components/imageComponents/imageList'; 4 | 5 | const ImageContainer = ({ toggle }) => { 6 | const [imageList, setImageList] = useState([]); 7 | const [updateImage, setUpdateImage] = useState(false); 8 | 9 | useEffect(async () => { 10 | const result = await imageService.getImageInfo(); 11 | setImageList(result); 12 | }, [updateImage]); 13 | 14 | return ( 15 |
    20 | 25 |
    26 | ); 27 | }; 28 | 29 | export default ImageContainer; 30 | -------------------------------------------------------------------------------- /client/containers/mainContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Nav from '../components/nav'; 3 | import Titlebar from '../components/titlebar'; 4 | import ContentContainer from './contentContainer'; 5 | import ImageContainer from './imageContainer'; 6 | import CreateImage from '../components/imageComponents/createImage'; 7 | import UserDbService from '../services/userDbService'; 8 | import ProtectedRoute from '../containers/protectedRoute'; 9 | 10 | import { 11 | BrowserRouter as Router, 12 | Switch, 13 | useRouteMatch, 14 | } from 'react-router-dom'; 15 | 16 | const MainContainer = (props) => { 17 | const [toggle, setToggle] = useState(true); 18 | const [isLogin, setIsLogin] = useState(false); 19 | const [userName, setUserName] = useState(''); 20 | useEffect(() => { 21 | let result; 22 | const getUser = async () => { 23 | result = await UserDbService.getUserToken(); 24 | if (result.token) { 25 | setIsLogin(true); 26 | setUserName(result.username); 27 | } 28 | }; 29 | getUser(); 30 | return () => { 31 | result = null; 32 | }; 33 | }, []); 34 | 35 | let main = useRouteMatch(); 36 | return ( 37 |
    38 | 45 |
    46 |
    47 | {toggle &&
    69 |
    70 |
    71 | ); 72 | }; 73 | export default MainContainer; 74 | -------------------------------------------------------------------------------- /client/containers/protectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect, Route } from 'react-router-dom'; 3 | import decode from 'jwt-decode'; 4 | import TokenStorage from '../db/token'; 5 | 6 | const ProtectedRoute = ({ component: Component, ...rest }) => { 7 | const token = TokenStorage.getToken(); 8 | const decodeToken = token ? decode(token) : false; 9 | const now = Math.floor(new Date().getTime() / 1000); 10 | const isNotExpired = decodeToken ? decodeToken.exp > now : false; 11 | 12 | return ( 13 | 16 | isNotExpired ? : 17 | } 18 | /> 19 | ); 20 | }; 21 | 22 | export default ProtectedRoute; 23 | -------------------------------------------------------------------------------- /client/containers/unProtectedRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Redirect, Route } from 'react-router-dom'; 3 | import decode from 'jwt-decode'; 4 | import TokenStorage from '../db/token'; 5 | 6 | const UnProtectedRoute = ({ component: Component, ...rest }) => { 7 | const token = TokenStorage.getToken(); 8 | const decodeToken = token ? decode(token) : false; 9 | const now = Math.floor(new Date().getTime() / 1000); 10 | const isNotExpired = decodeToken ? decodeToken.exp > now : false; 11 | 12 | return ( 13 | 16 | isNotExpired ? : 17 | } 18 | /> 19 | ); 20 | }; 21 | 22 | export default UnProtectedRoute; 23 | -------------------------------------------------------------------------------- /client/db/token.js: -------------------------------------------------------------------------------- 1 | const TOKEN = 'token'; 2 | class TokenStorage { 3 | static saveToken(token) { 4 | localStorage.setItem(TOKEN, token); 5 | } 6 | 7 | static getToken() { 8 | return localStorage.getItem(TOKEN); 9 | } 10 | 11 | static clearToken() { 12 | localStorage.clear(TOKEN); 13 | } 14 | 15 | s; 16 | } 17 | 18 | export default TokenStorage; 19 | -------------------------------------------------------------------------------- /client/db/user.js: -------------------------------------------------------------------------------- 1 | const USER = 'user'; 2 | 3 | export default class UserStorage { 4 | saveUser(user) { 5 | localStorage.setItem(USER, user); 6 | } 7 | 8 | getUser() { 9 | return localStorage.getItem(USER); 10 | } 11 | 12 | clearUser() { 13 | localStorage.clear(USER); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import App from './App'; 5 | import store from './store'; 6 | import styles from './scss/application.scss'; 7 | 8 | // render(, document.getElementById('root')); 9 | 10 | // Once we make files with redux, we can change the code to below. 11 | render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /client/listButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Output = (props) => { 4 | 5 | return ( 6 |
    7 | 8 |
    9 | ); 10 | }; 11 | 12 | export default Output; -------------------------------------------------------------------------------- /client/redux/action/action.js: -------------------------------------------------------------------------------- 1 | import * as types from './actionTypes'; 2 | 3 | export const getContainerInfo = (payload) => ({ 4 | type: types.CONTAINER_INFO_GET, 5 | payload, 6 | }); 7 | 8 | export const setStateMetrics = (payload) => ({ 9 | type: types.SET_STATE_METRICS, 10 | payload, 11 | }); 12 | 13 | export const setTimeSelector = (payload) => ({ 14 | type: types.TIME_SELECTOR, 15 | payload, 16 | }) 17 | -------------------------------------------------------------------------------- /client/redux/action/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const CONTAINER_INFO_GET = 'CONTAINER_INFO_GET'; 2 | export const SET_STATE_METRICS = 'SET_STATE_METRICS'; 3 | export const TIME_SELECTOR = 'TIME_SELECTOR'; -------------------------------------------------------------------------------- /client/redux/reducers/containerReducer.js: -------------------------------------------------------------------------------- 1 | import * as types from '../action/actionTypes'; 2 | 3 | const initialState = { 4 | containerList: [], 5 | metrics: [], 6 | time: 1 7 | }; 8 | 9 | export const containerReducer = (state = initialState, action) => { 10 | switch (action.type) { 11 | case types.CONTAINER_INFO_GET: { 12 | return { 13 | ...state, 14 | containerList: action.payload, 15 | }; 16 | } 17 | case types.SET_STATE_METRICS: 18 | const newMetrics = action.payload; 19 | return { 20 | ...state, 21 | metrics: newMetrics 22 | } 23 | case types.TIME_SELECTOR: { 24 | return { 25 | ...state, 26 | time: action.payload 27 | } 28 | } 29 | default: { 30 | return state; 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /client/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { containerReducer } from './containerReducer'; 3 | 4 | export const reducers = combineReducers({ 5 | containers: containerReducer, 6 | }); 7 | -------------------------------------------------------------------------------- /client/scss/application.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'mainContainer'; 3 | @import 'nav'; 4 | @import 'dockerCommand'; 5 | @import 'stats'; 6 | @import 'containerList'; 7 | @import 'createImage'; 8 | @import 'login'; 9 | @import 'titlebar'; 10 | @import 'images'; 11 | @import 'userStatus'; 12 | -------------------------------------------------------------------------------- /client/scss/containerList.scss: -------------------------------------------------------------------------------- 1 | .container_list { 2 | background-color: $background-color; 3 | text-align: center; 4 | } 5 | 6 | .container_head { 7 | display: flex; 8 | justify-content: space-around; 9 | width: 100%; 10 | } 11 | 12 | .container_item { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | height: 5.2rem; 17 | border: 1px solid darken($background-color, 10%); 18 | background-color: darken($background-color, 3%); 19 | border-radius: 10px; 20 | margin: 1rem 0; 21 | padding: 1rem 1.7rem 1rem 0.8rem; 22 | } 23 | 24 | .item_name_time { 25 | display: flex; 26 | width: 100%; 27 | justify-content: space-between; 28 | } 29 | 30 | .item_check { 31 | display: flex; 32 | align-items: center; 33 | } 34 | 35 | .item_checkbox { 36 | width: 1rem; 37 | height: 1rem; 38 | } 39 | 40 | .checkbox_invisible { 41 | visibility: hidden; 42 | } 43 | 44 | .item_name { 45 | font-weight: bold; 46 | margin-left: 0.8rem; 47 | font-size: $font-medium; 48 | } 49 | 50 | .item_createdat { 51 | font-weight: lighter; 52 | font-size: $font-small; 53 | } 54 | 55 | .item_state_dateBtn { 56 | display: flex; 57 | width: 100%; 58 | justify-content: space-between; 59 | align-items: center; 60 | padding: 0.8rem 0 0 0.5rem; 61 | } 62 | 63 | .item_state { 64 | height: 1.4rem; 65 | padding: 0.15rem 0.6rem; 66 | border-radius: 10px; 67 | font-size: $font-small; 68 | margin-left: 1.2rem; 69 | } 70 | 71 | .is_running { 72 | background-color: darken($background-color, 3%); 73 | border: 1px solid darken($green-color, 5%); 74 | color: darken($green-color, 5%); 75 | } 76 | 77 | .is_exited { 78 | background-color: darken($background-color, 3%); 79 | border: 1px solid lighten($orange-color, 4%); 80 | color: lighten($orange-color, 4%); 81 | } 82 | 83 | .is_else { 84 | background-color: darken($background-color, 3%); 85 | border: 1px solid lighten($accent-color, 3%); 86 | color: lighten($accent-color, 3%); 87 | } 88 | 89 | .item_dataBtn { 90 | padding: 0.25rem 0.65rem; 91 | color: $white-color; 92 | background-color: lighten($primary-color, 5%); 93 | border: 1px solid $primary-color; 94 | border-radius: 10px; 95 | font-size: $font-small; 96 | } 97 | 98 | .item_dataBtn:hover { 99 | background-color: saturate($primary-color, 10%); 100 | border: 1px solid lighten($primary-color, 5%); 101 | } 102 | 103 | .item_dataBtn:focus { 104 | background-color: saturate($primary-color, 10%); 105 | border: 1px solid lighten($primary-color, 5%); 106 | } 107 | -------------------------------------------------------------------------------- /client/scss/createImage.scss: -------------------------------------------------------------------------------- 1 | .create_image { 2 | display: flex; 3 | justify-content: space-around; 4 | height: 95vh; 5 | background-color: $background-color; 6 | overflow-y: auto; 7 | } 8 | 9 | .create_toggle { 10 | width: 84vw; 11 | } 12 | 13 | .create_toggle_inactive { 14 | width: 100vw; 15 | } 16 | 17 | .editor_con { 18 | margin: 2rem 0; 19 | width: 28rem; 20 | display: flex; 21 | flex-direction: column; 22 | } 23 | 24 | .YAML.collapsed { 25 | height: 0; 26 | width: 0; 27 | } 28 | 29 | .Dockerfile.collapsed { 30 | height: 0; 31 | width: 0; 32 | } 33 | 34 | .editor_title { 35 | display: flex; 36 | justify-content: space-between; 37 | background-color: darken($accent-background-color, 4%); 38 | padding: 0.5rem 0.5rem 0.5rem 1rem; 39 | border-top-right-radius: 0.5rem; 40 | border-top-left-radius: 0.5rem; 41 | color: darken($font-color, 10%); 42 | } 43 | 44 | .editor_openBtn { 45 | background-color: darken($accent-background-color, 4%); 46 | color: darken($font-color, 10%); 47 | font-size: $font-small; 48 | padding: 3px; 49 | border: 1px solid darken($font-color, 50%); 50 | border-radius: 6px; 51 | } 52 | .editor_openBtn:hover { 53 | background-color: $primary-color; 54 | color: darken($font-color, 10%); 55 | border: 1px solid darken($primary-color, 10%); 56 | border-radius: 6px; 57 | } 58 | .editor_openBtn:focus { 59 | color: darken($font-color, 10%); 60 | border: 1px solid darken($font-color, 50%); 61 | border-radius: 6px; 62 | } 63 | 64 | .code-mirror-wrapper { 65 | // flex-grow: 1; 66 | overflow: hidden; 67 | } 68 | 69 | .editor_save { 70 | background-color: darken($accent-background-color, 4%); 71 | padding: 0.5rem 0.5rem 0.5rem 1rem; 72 | color: darken($font-color, 10%); 73 | border-bottom-right-radius: 0.5rem; 74 | border-bottom-left-radius: 0.5rem; 75 | } 76 | -------------------------------------------------------------------------------- /client/scss/dockerCommand.scss: -------------------------------------------------------------------------------- 1 | .content_container { 2 | display: flex; 3 | } 4 | .docker_command { 5 | height: 95vh; 6 | padding: 1rem 1.5rem; 7 | background-color: $background-color; 8 | overflow-y: auto; 9 | } 10 | 11 | .content_toggle { 12 | width: 37vw; 13 | } 14 | 15 | .content_toggle_inactive { 16 | width: 53vw; 17 | } 18 | 19 | .docker_buttons { 20 | display: flex; 21 | align-items: center; 22 | height: 6vh; 23 | margin-bottom: 0.1rem; 24 | } 25 | 26 | .docker_btn { 27 | margin: 2px; 28 | padding: 0.25rem 0.5rem; 29 | font-size: $font-small; 30 | border-radius: 5px; 31 | } 32 | 33 | .docker_start { 34 | background-color: transparentize($green-color, 0.3%); 35 | color: $white-color; 36 | } 37 | 38 | .docker_start:hover { 39 | background-color: $green-color; 40 | color: $white-color; 41 | } 42 | 43 | .docker_start:focus { 44 | background-color: transparentize($green-color, 0.3%); 45 | color: $white-color; 46 | } 47 | 48 | .docker_redbtn { 49 | background-color: transparentize($orange-color, 0.3%); 50 | color: $white-color; 51 | } 52 | 53 | .docker_redbtn:hover { 54 | background-color: lighten($orange-color, 2%); 55 | color: $white-color; 56 | } 57 | 58 | .docker_redbtn:focus { 59 | background-color: transparentize($orange-color, 0.3%); 60 | color: $white-color; 61 | } 62 | 63 | .docker_commonbtn { 64 | background-color: lighten($background-color, 12%); 65 | color: $white-color; 66 | } 67 | 68 | .docker_commonbtn:hover { 69 | background-color: lighten($accent-color, 10%); 70 | color: $white-color; 71 | } 72 | 73 | .docker_commonbtn:focus { 74 | background-color: transparentize($accent-color, 0.5%); 75 | color: $white-color; 76 | } 77 | 78 | .add_btn { 79 | margin-bottom: 0.8rem; 80 | background-color: darken($accent-color, 20%); 81 | color: $white-color; 82 | } 83 | 84 | .add_btn:hover { 85 | background-color: lighten($accent-color, 10%); 86 | color: $white-color; 87 | } 88 | 89 | .add_btn:focus { 90 | background-color: darken($accent-color, 20%); 91 | color: $white-color; 92 | } 93 | -------------------------------------------------------------------------------- /client/scss/images.scss: -------------------------------------------------------------------------------- 1 | .image_container { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | background-color: $background-color; 6 | height: 95vh; 7 | padding: 2.5rem; 8 | overflow-y: auto; 9 | } 10 | 11 | .image_toggle { 12 | width: 84vw; 13 | } 14 | 15 | .image_toggle_inactive { 16 | width: 100vw; 17 | } 18 | 19 | .image_main { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | } 24 | 25 | .image_select { 26 | display: flex; 27 | align-items: center; 28 | } 29 | 30 | .select_opt { 31 | margin-right: 0.6rem; 32 | border-radius: 5px; 33 | height: 1.9rem; 34 | width: 4.5rem; 35 | background-color: lighten($background-color, 12%); 36 | color: $font-color; 37 | outline: none; 38 | } 39 | 40 | .image_pull { 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | .image_input { 46 | height: 2.1rem; 47 | width: 15rem; 48 | margin-right: 0.8rem; 49 | border: 1px solid lighten($background-color, 26%); 50 | border-radius: 5px; 51 | background-color: lighten($background-color, 12%); 52 | color: $font-color; 53 | } 54 | 55 | ::placeholder { 56 | color: $font-color; 57 | } 58 | 59 | .image_or { 60 | font-size: $font-small; 61 | margin: 0 0.5rem; 62 | } 63 | 64 | .image_submit { 65 | height: 2rem; 66 | padding: 0 0.5rem; 67 | background-color: lighten($primary-color, 5%); 68 | border: 1px solid lighten($primary-color, 5%); 69 | color: $white-color; 70 | border-radius: 5px; 71 | font-weight: bold; 72 | } 73 | 74 | .image_submit:hover { 75 | background-color: saturate($primary-color, 5%); 76 | border: 1px solid darken($primary-color, 30%); 77 | color: $white-color; 78 | } 79 | 80 | .image_submit:focus { 81 | background-color: lighten($primary-color, 5%); 82 | border: 1px solid darken($primary-color, 30%); 83 | color: $white-color; 84 | } 85 | 86 | .build_textbox { 87 | height: 2.1rem; 88 | width: 12rem; 89 | margin-right: 0.5rem; 90 | border: 1px solid lighten($background-color, 26%); 91 | border-radius: 5px; 92 | background-color: lighten($background-color, 12%); 93 | color: $font-color; 94 | } 95 | 96 | .image_list { 97 | display: flex; 98 | width: 35rem; 99 | flex-wrap: wrap; 100 | justify-content: space-between; 101 | margin: 1rem; 102 | } 103 | 104 | .image_item { 105 | display: flex; 106 | flex-direction: column; 107 | align-items: center; 108 | justify-content: center; 109 | height: 7rem; 110 | width: 17rem; 111 | padding: 10px; 112 | margin: 0.5rem 0; 113 | border: 1px solid darken($background-color, 10%); 114 | background-color: darken($background-color, 3%); 115 | border-radius: 5px; 116 | } 117 | 118 | .image_tagOpt { 119 | display: flex; 120 | justify-content: space-between; 121 | align-items: center; 122 | width: 100%; 123 | } 124 | 125 | .image_tag { 126 | margin-bottom: 0.3rem; 127 | font-weight: 500; 128 | color: lighten($accent-background-color, 60%); 129 | font-size: $font-small; 130 | } 131 | 132 | .image_opt { 133 | padding: 3px 4px; 134 | border: 1px solid darken($background-color, 3%); 135 | } 136 | 137 | .image_opt:hover { 138 | color: lighten($primary-color, 10%); 139 | border: 1px solid lighten($primary-color, 10%); 140 | border-radius: 5px; 141 | } 142 | 143 | .image_name { 144 | margin-bottom: 1rem; 145 | font-size: $font-medium; 146 | } 147 | 148 | .image_button { 149 | margin: 0.2rem 0.3rem; 150 | padding: 0.3rem 1.2rem; 151 | border-radius: 40px; 152 | font-size: $font-small; 153 | } 154 | 155 | .image_start { 156 | color: darken($background-color, 3%); 157 | background-color: transparentize($secondary-color, 0.1%); 158 | font-weight: bold; 159 | } 160 | 161 | .image_start:hover { 162 | background-color: saturate($secondary-color, 30%); 163 | box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.24); 164 | color: $white-color; 165 | font-weight: bold; 166 | transform: scale(1.03); 167 | } 168 | 169 | .image_running { 170 | width: 80px; 171 | height: 26px; 172 | margin: 0.2rem 0.3rem; 173 | padding-top: 2px; 174 | border-radius: 2rem; 175 | color: darken($green-color, 10%); 176 | background-color: lighten($green-color, 42%); 177 | border: 2px solid darken($green-color, 10%); 178 | text-align: center; 179 | font-size: $font-small; 180 | } 181 | 182 | .imagebtns { 183 | display: flex; 184 | } 185 | 186 | .opt_box { 187 | position: absolute; 188 | margin-left: 16rem; 189 | width: 4.5rem; 190 | height: 2.5rem; 191 | background-color: lighten($background-color, 10%); 192 | border: 1px solid lighten($background-color, 20%); 193 | border-radius: 5px; 194 | text-align: center; 195 | padding: 0.6rem 0; 196 | } 197 | 198 | .image_remove { 199 | // height: 1.5rem; 200 | color: $white-color; 201 | font-size: $font-small; 202 | background-color: lighten($background-color, 10%); 203 | } 204 | 205 | .image_remove:hover { 206 | // background-color: lighten($background-color, 50%); 207 | color: $orange-color; 208 | font-weight: bolder; 209 | transform: scale(1.1); 210 | } 211 | -------------------------------------------------------------------------------- /client/scss/login.scss: -------------------------------------------------------------------------------- 1 | .login_page { 2 | display: flex; 3 | height: 95vh; 4 | } 5 | 6 | .login_wallpaper { 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | width: 40vw; 12 | height: 100%; 13 | background-color: $accent-background-color; 14 | color: darken($font-color, 8%); 15 | } 16 | 17 | .login_head { 18 | margin-bottom: 1rem; 19 | font-size: $font-head; 20 | font-weight: bold; 21 | } 22 | 23 | .login_intro { 24 | font-size: $font-medium; 25 | margin: 0.2rem 0; 26 | } 27 | 28 | .login_wall_btn { 29 | margin-top: 4rem; 30 | font-size: $font-medium; 31 | width: 200px; 32 | height: 50px; 33 | background-color: $accent-background-color; 34 | color: $font-color; 35 | border-radius: 30px; 36 | border: 1px solid $font-color; 37 | } 38 | 39 | .login_wall_btn:hover { 40 | transform: scale(1.1); 41 | background-color: $primary-color; 42 | border: none; 43 | } 44 | 45 | .login_wall_btn:focus { 46 | transform: scale(1.1); 47 | background-color: $accent-background-color; 48 | border: none; 49 | } 50 | 51 | .signin_page { 52 | display: flex; 53 | flex-direction: column; 54 | justify-content: center; 55 | align-items: center; 56 | width: 60vw; 57 | height: 100%; 58 | background-color: lighten($background-color, 5%); 59 | } 60 | 61 | .signin_head { 62 | font-size: $font-head; 63 | font-weight: bold; 64 | color: $white-color; 65 | margin-bottom: 4rem; 66 | } 67 | 68 | .signin_form { 69 | display: flex; 70 | flex-direction: column; 71 | margin-bottom: 1rem; 72 | } 73 | 74 | .signin_forgotPW { 75 | padding-bottom: 0.5rem; 76 | border-bottom: 2px solid $font-dark-color; 77 | font-size: $font-medium; 78 | } 79 | 80 | .signin_input { 81 | width: 340px; 82 | height: 48px; 83 | margin: 0.3rem; 84 | border: 1px solid lighten($background-color, 26%); 85 | border-radius: 5px; 86 | background-color: lighten($background-color, 16%); 87 | color: $font-color; 88 | } 89 | 90 | .login_error { 91 | color: $orange-color; 92 | font-size: $font-small; 93 | padding: 0.5rem 1rem 0; 94 | } 95 | 96 | .signin_btn { 97 | margin-top: 2rem; 98 | font-size: $font-medium; 99 | width: 200px; 100 | height: 50px; 101 | background-color: $primary-color; 102 | color: $white-color; 103 | border-radius: 30px; 104 | } 105 | 106 | .signin_btn:hover { 107 | transform: scale(1.1); 108 | background-color: lighten($primary-color, 5%); 109 | } 110 | 111 | .signin_btn:focus { 112 | background-color: $primary-color; 113 | } 114 | -------------------------------------------------------------------------------- /client/scss/mainContainer.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: 'Roboto', sans-serif; 9 | color: $font-color; 10 | } 11 | 12 | button, 13 | button:focus { 14 | border: none; 15 | outline: none; 16 | cursor: pointer; 17 | } 18 | 19 | li { 20 | list-style: none; 21 | } 22 | 23 | a { 24 | text-decoration: none; 25 | } 26 | input { 27 | padding: 0.8rem 0.6rem; 28 | outline: none; 29 | } 30 | 31 | .main_container { 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | 36 | .nav_content { 37 | display: flex; 38 | } 39 | -------------------------------------------------------------------------------- /client/scss/nav.scss: -------------------------------------------------------------------------------- 1 | .nav { 2 | display: flex; 3 | flex-direction: column; 4 | width: 16vw; 5 | min-width: 9rem; 6 | background-color: $accent-background-color; 7 | height: 95vh; 8 | padding: 2.5rem 0; 9 | } 10 | 11 | .nav_icon { 12 | width: 5rem; 13 | height: auto; 14 | align-self: center; 15 | margin: 0 0 2.5rem 0; 16 | } 17 | 18 | .nav_btns { 19 | text-align: start; 20 | width: 15vw; 21 | min-width: 8.5rem; 22 | background-color: $accent-background-color; 23 | color: $font-color; 24 | font-size: $font-nav; 25 | padding: 1rem 0 1rem 2rem; 26 | border-radius: 0 10px 10px 0; 27 | } 28 | 29 | .nav_btns:hover { 30 | background-color: lighten($accent-background-color, 5%); 31 | border-left: 0.5rem solid lighten($primary-color, 30%); 32 | padding: 1rem 0 1rem 1.5rem; 33 | color: $font-color; 34 | } 35 | 36 | .nav_btns:focus { 37 | background-color: transparentize($primary-color, 0.6%); 38 | color: $white-color; 39 | font-weight: bold; 40 | } 41 | -------------------------------------------------------------------------------- /client/scss/stats.scss: -------------------------------------------------------------------------------- 1 | .stats { 2 | background-color: $background-color; 3 | color: $gray-color; 4 | display: flex; 5 | width: 47vw; 6 | flex-direction: column; 7 | align-items: center; 8 | height: 95vh; 9 | padding: 3rem 1rem; 10 | overflow-y: auto; 11 | border-left: 1px solid lighten($background-color, 10%); 12 | 13 | #stats_time { 14 | display: flex; 15 | flex-direction: row; 16 | width: 80%; 17 | justify-content: space-evenly; 18 | padding: 1rem 0.33rem; 19 | } 20 | 21 | .stats_graph_dataKey { 22 | color: $font-color; 23 | } 24 | 25 | h3 { 26 | text-align: center; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/scss/titlebar.scss: -------------------------------------------------------------------------------- 1 | .titlebar { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 100%; 6 | height: 5vh; 7 | background-color: darken($accent-background-color, 3%); 8 | border-bottom: 1px solid lighten($background-color, 10%); 9 | } 10 | 11 | .title_bar { 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | -webkit-app-region: drag; 16 | width: 98vw; 17 | height: 3.5vh; 18 | color: $font-color; 19 | } 20 | 21 | .titlebar_traffic { 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | .titlebar_toggle { 27 | width: 2.5vh; 28 | height: 2.5vh; 29 | font-size: 2.5vh; 30 | cursor: pointer; 31 | margin-left: 13vw; 32 | } 33 | 34 | .titlebar_toggle:hover { 35 | color: $white-color; 36 | cursor: pointer; 37 | } 38 | 39 | .titlebar_logo { 40 | width: 6vw; 41 | margin-left: 1.5rem; 42 | } 43 | 44 | .titlebar_btns { 45 | display: flex; 46 | align-items: center; 47 | font-size: 2.5vh; 48 | margin-right: 3vw; 49 | } 50 | 51 | .titlebar_link { 52 | width: 2.8vh; 53 | height: 2.8vh; 54 | margin: 0 15px; 55 | } 56 | 57 | .titlebar_icon { 58 | width: 3vh; 59 | height: 3vh; 60 | } 61 | 62 | .titlebar_btn { 63 | width: 2.5vh; 64 | height: 2.5vh; 65 | margin: 0 15px; 66 | cursor: pointer; 67 | } 68 | 69 | .titlebar_btn:hover { 70 | color: $white-color; 71 | cursor: pointer; 72 | } 73 | -------------------------------------------------------------------------------- /client/scss/userStatus.scss: -------------------------------------------------------------------------------- 1 | .user_status { 2 | width: 200px; 3 | position: absolute; 4 | margin-left: 79vw; 5 | z-index: 1; 6 | padding: 0.5rem 0; 7 | background-color: darken($background-color, 4%); 8 | border-radius: 5px; 9 | border: 1px solid darken($background-color, 10%); 10 | box-shadow: 0 0 10px rgba(26, 25, 25, 0.2); 11 | color: $font-color; 12 | } 13 | 14 | .starus_user_name { 15 | margin: 1rem 1.5rem; 16 | font-weight: bolder; 17 | } 18 | 19 | .status_block { 20 | width: 100%; 21 | padding: 0.5rem 0; 22 | } 23 | 24 | .block_line { 25 | border-bottom: 1.5px solid lighten($background-color, 10%); 26 | } 27 | 28 | .user_btn { 29 | text-align: left; 30 | width: 100%; 31 | height: 30px; 32 | cursor: pointer; 33 | padding: 0 1.5rem; 34 | margin: 1rem 0 0; 35 | color: $font-color; 36 | background-color: darken($background-color, 5%); 37 | } 38 | 39 | .user_btn:hover { 40 | background-color: transparentize($primary-color, 0.3%); 41 | color: $white-color; 42 | } 43 | -------------------------------------------------------------------------------- /client/scss/variables.scss: -------------------------------------------------------------------------------- 1 | $white-color: #ffffff; 2 | $black-color: #000000; 3 | $gray-color: #f7f7f7; 4 | $darker-color: #032244; 5 | $primary-color: #4741c0; 6 | $secondary-color: #47c796; 7 | $accent-color: #0091e2; 8 | $background-color: #2c2f35; 9 | $accent-background-color: #202631; 10 | $font-color: #dbdbdb; 11 | $font-dark-color: #1b1b1b; 12 | $yellow-color: #ffe551; 13 | $green-color: rgb(66, 233, 102); 14 | $orange-color: #fa4b2cef; 15 | $font-head: 32px; 16 | $font-nav: 1em; 17 | $font-large: 17px; 18 | $font-medium: 15px; 19 | $font-small: 12px; 20 | $font-micro: 11px; 21 | -------------------------------------------------------------------------------- /client/services/axiosService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import TokenStorage from '../db/token'; 3 | 4 | class axiosService { 5 | static authHeaders() { 6 | const token = TokenStorage.getToken(); 7 | return { 8 | headers: { Authorization: `Bearer ${token}` }, 9 | }; 10 | } 11 | 12 | static getRequest(url, ...args) { 13 | return axios.get(url, ...args, axiosService.authHeaders()); 14 | } 15 | 16 | static postRequest(url, ...args) { 17 | return axios.post(url, ...args, axiosService.authHeaders()); 18 | } 19 | } 20 | 21 | export default axiosService; 22 | -------------------------------------------------------------------------------- /client/services/containerService.js: -------------------------------------------------------------------------------- 1 | import axiosService from './axiosService'; 2 | 3 | class ContainerService { 4 | static async setupCon() { 5 | try { 6 | await axiosService.getRequest('http://localhost:3000/api/containers'); 7 | } catch (err) { 8 | throw new Error( 9 | 'There was an error setting up Containers from services/containerService' 10 | ); 11 | } 12 | } 13 | 14 | static async getConInfo() { 15 | try { 16 | let result = await axiosService.getRequest( 17 | 'http://localhost:3000/api/containers/containers' 18 | ); 19 | return result.data; 20 | } catch (err) { 21 | throw new Error( 22 | 'There was an error getting container information from services/containerService' 23 | ); 24 | } 25 | } 26 | 27 | static async getMetrics(id, time) { 28 | try { 29 | const memoryStats = await axiosService.getRequest( 30 | 'http://localhost:3000/api/metrics', 31 | { 32 | params: { 33 | id, 34 | start: time, 35 | query: `container_memory_usage_bytes{id=~'/docker/${id}'}`, 36 | }, 37 | } 38 | ); 39 | 40 | const machineMem = await axiosService.getRequest( 41 | 'http://localhost:3000/api/metrics', 42 | { 43 | params: { 44 | id, 45 | start: time, 46 | query: `machine_memory_bytes`, 47 | }, 48 | } 49 | ); 50 | 51 | const cpuStats = await axiosService.getRequest( 52 | 'http://localhost:3000/api/metrics', 53 | { 54 | params: { 55 | id, 56 | start: time, 57 | query: `sum(rate(container_cpu_usage_seconds_total {id=~'/docker/${id}'} [5m]))`, 58 | }, 59 | } 60 | ); 61 | 62 | const cores = await axiosService.getRequest( 63 | 'http://localhost:3000/api/metrics', 64 | { 65 | params: { 66 | id, 67 | start: time, 68 | query: 'machine_cpu_cores', 69 | }, 70 | } 71 | ); 72 | const coreCount = cores.data[0][1]; 73 | const data = {}; 74 | data.memory = []; 75 | data.cpu = []; 76 | 77 | memoryStats.data.forEach((dataPoint, i) => { 78 | const machineMemory = machineMem.data[0][1]; 79 | const time = new Date(dataPoint[0] * 1000); 80 | data.memory.push({ 81 | time: time.toTimeString().slice(0, 5), 82 | percentTotalMemoryUsed: (dataPoint[1] / machineMemory) * 100, 83 | }); 84 | }); 85 | 86 | cpuStats.data.forEach((dataPoint) => { 87 | const time = new Date(dataPoint[0] * 1000); 88 | data.cpu.push({ 89 | time: time.toTimeString().slice(0, 5), 90 | percentTotalCpuUsed: (dataPoint[1] / coreCount) * 100, 91 | }); 92 | }); 93 | 94 | return data; 95 | } catch (err) { 96 | throw new Error( 97 | 'There was an error getting container information from services/containerService: ' 98 | ); 99 | } 100 | } 101 | 102 | static postClickBtn(command, id) { 103 | try { 104 | const result = axiosService.postRequest( 105 | `http://localhost:3000/api/containers/${command}`, 106 | { containerID: id } 107 | ); 108 | return result; 109 | } catch (err) { 110 | throw new Error( 111 | 'There is error on button functions in container service' 112 | ); 113 | } 114 | } 115 | } 116 | 117 | export default ContainerService; 118 | -------------------------------------------------------------------------------- /client/services/containerService.test.js: -------------------------------------------------------------------------------- 1 | import Service from './containerService'; 2 | import axios from 'axios'; 3 | 4 | describe('>> Container Service', () => { 5 | describe('>> getConInfo', () => { 6 | it('should call url with Axios', async () => { 7 | const spy = jest.spyOn(axios, 'get'); 8 | const url = '/any-url-you-want'; 9 | await Service.getConInfo(url); 10 | 11 | expect(spy).toBeCalledWith(url); 12 | }); 13 | 14 | // it('should call return err string ', async () => { 15 | // const spy = jest.spyOn(console, 'log'); 16 | // const url = '/any-url-you-want'; 17 | // const message = await Service.getConInfo(url).rejects.toThrowError( 18 | // new InvalidArgumentError('Some error message') 19 | // ); 20 | 21 | // expect(message).toBeCalledWith('err'); 22 | // }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /client/services/imageService.js: -------------------------------------------------------------------------------- 1 | import axiosService from './axiosService'; 2 | 3 | class ImageService { 4 | static async getImageInfo() { 5 | try { 6 | const result = await axiosService.getRequest( 7 | 'http://localhost:3000/api/images' 8 | ); 9 | return result.data; 10 | } catch (error) { 11 | throw new Error( 12 | 'There was an error getting image information from services/imageService: ' 13 | ); 14 | } 15 | } 16 | 17 | static async pullImageInfo(image) { 18 | try { 19 | const result = await axiosService.postRequest( 20 | 'http://localhost:3000/api/images/pull', 21 | { 22 | imageName: image, 23 | } 24 | ); 25 | return result; 26 | } catch (error) { 27 | throw new Error('There was an error pulling Image Information: ', error); 28 | } 29 | } 30 | 31 | static async startImage(ID) { 32 | try { 33 | const result = await axiosService.postRequest( 34 | 'http://localhost:3000/api/images/start', 35 | { imageID: ID } 36 | ); 37 | return result; 38 | } catch (error) { 39 | throw new Error('There was an error starting the Image: ', error); 40 | } 41 | } 42 | 43 | static async deleteImage(ID) { 44 | try { 45 | const result = await axiosService.postRequest( 46 | 'http://localhost:3000/api/images/delete', 47 | { imageID: ID } 48 | ); 49 | return result; 50 | } catch (error) { 51 | throw new Error('There was an error starting the Image: ', error); 52 | return error; 53 | } 54 | } 55 | 56 | static async DockerFileDefaultText() { 57 | return boilerPlate; 58 | } 59 | 60 | static async buildImage(info) { 61 | try { 62 | const result = await axiosService.postRequest( 63 | 'http://localhost:3000/api/images/build', 64 | { 65 | imageName: info.imageName, 66 | path: info.dockerPath, 67 | } 68 | ); 69 | return result; 70 | } catch (error) { 71 | throw new Error('There was an error building the image: ', error); 72 | return error; 73 | } 74 | } 75 | 76 | static dockerBoiler() { 77 | const dockerBoiler = `FROM 78 | 79 | WORKDIR 80 | 81 | COPY 82 | 83 | COPY 84 | 85 | RUN 86 | 87 | COPY 88 | 89 | CMD 90 | `; 91 | 92 | return dockerBoiler; 93 | } 94 | 95 | static yamlBoiler() { 96 | const yamlBoiler = `version: 97 | services: 98 | web: 99 | build: . 100 | ports: 101 | - "5000:5000" 102 | volumes: 103 | - .:/code 104 | - logvolume01:/var/log 105 | volumes: 106 | logvolume01: {} 107 | `; 108 | 109 | return yamlBoiler; 110 | } 111 | } 112 | 113 | export default ImageService; 114 | -------------------------------------------------------------------------------- /client/services/prometheusService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | class StartUp { 4 | static async prometheus() { 5 | try { 6 | await axios.get('http://localhost:3000/metrics/node-exporter'); 7 | await axios.get('http://localhost:3000/metrics/yamlParse'); 8 | return; 9 | } catch (error) { 10 | if (error) { 11 | console.log('Error in startup of prometheus: ', error); 12 | } 13 | } 14 | } 15 | } 16 | 17 | export default StartUp; -------------------------------------------------------------------------------- /client/services/userDbService.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import TokenStorage from '../db/token'; 3 | 4 | class UserDbService { 5 | static async postUserData(userData) { 6 | try { 7 | let result = await axios.post( 8 | 'http://localhost:3000/api/user/login', 9 | userData 10 | ); 11 | return result.data; 12 | } catch (error) { 13 | console.log('this error is from User Service: ', error); 14 | return { error: true }; 15 | } 16 | } 17 | 18 | static async getUserToken() { 19 | const token = TokenStorage.getToken(); 20 | try { 21 | const result = await axios.get('http://localhost:3000/api/user/me', { 22 | headers: { Authorization: `Bearer ${token}` }, 23 | }); 24 | return result.data; 25 | } catch (error) { 26 | console.log('There was an error getting user token: ', error); 27 | return { error: 'unauthenticated' }; 28 | } 29 | } 30 | 31 | static logout() { 32 | TokenStorage.clearToken(); 33 | } 34 | } 35 | 36 | export default UserDbService; 37 | -------------------------------------------------------------------------------- /client/services/utilities.js: -------------------------------------------------------------------------------- 1 | export const throttle = (fn, time) => { 2 | let timeoutId = null; 3 | 4 | return function () { 5 | if (timeoutId !== null) { 6 | return; 7 | } 8 | fn(...arguments); 9 | timeoutId = setTimeout(() => { 10 | timeoutId = null; 11 | }, time); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /client/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { composeWithDevTools } from 'redux-devtools-extension'; 3 | import { reducers } from './redux/reducers'; 4 | 5 | const store = createStore(reducers, composeWithDevTools()); 6 | 7 | export default store; 8 | -------------------------------------------------------------------------------- /electron/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain, Menu } = require('electron'); 2 | const path = require('path'); 3 | const url = require('url'); 4 | const server = require('../server/server.js'); 5 | 6 | require('../server/server'); 7 | 8 | let mainWindow; 9 | 10 | function createWindow() { 11 | mainWindow = new BrowserWindow({ 12 | width: 1200, 13 | height: 770, 14 | minWidth: 1040, 15 | minHeight: 680, 16 | // frame: false, 17 | titleBarStyle: 'hiddenInset', 18 | webPreferences: { 19 | nodeIntegration: true, 20 | contextIsolation: false, 21 | enableRemoteModule: true, 22 | }, 23 | }); 24 | 25 | if (process.env.NODE_ENV === 'development') { 26 | // when it is in development mode, it opens in browser. 27 | mainWindow.webContents.openDevTools(); 28 | mainWindow.loadURL(`http://localhost:3000`); 29 | } else { 30 | // when it is production mode, we are opening the electron app 31 | // need to yarn build first, and then yarn start. 32 | mainWindow.loadURL( 33 | url.format({ 34 | pathname: path.join(__dirname, '../index.html'), 35 | protocol: 'file:', 36 | slashes: true, 37 | }) 38 | ); 39 | } 40 | 41 | Menu.setApplicationMenu(null); 42 | 43 | // mainWindow.on('closed', () => { 44 | // mainWindow = null; 45 | // }); 46 | 47 | // mainWindow.on('maximize', () => { 48 | // mainWindow.webContents.send('maximized'); 49 | // }); 50 | 51 | // mainWindow.on('unmaximize', () => { 52 | // mainWindow.webContents.send('unmaximized'); 53 | // }); 54 | } 55 | 56 | // ipcMain.handle('minimize-event', () => { 57 | // mainWindow.minimize(); 58 | // }); 59 | 60 | // ipcMain.handle('maximize-event', () => { 61 | // mainWindow.maximize(); 62 | // }); 63 | 64 | // ipcMain.handle('unmaximize-event', () => { 65 | // mainWindow.unmaximize(); 66 | // }); 67 | 68 | // ipcMain.handle('close-event', () => { 69 | // app.quit(); 70 | // }); 71 | 72 | // app.on('browser-window-focus', () => { 73 | // mainWindow.webContents.send('focused'); 74 | // }); 75 | 76 | // app.on('browser-window-blur', () => { 77 | // mainWindow.webContents.send('blurred'); 78 | // }); 79 | 80 | app.whenReady().then(() => { 81 | createWindow(); 82 | 83 | app.on('activate', () => { 84 | // on macOS it is common to re-create a window in the app when the dock icon is clicked and there are no other windows open. 85 | if (BrowserWindow.getAllWindows().length === 0) createWindow(); 86 | }); 87 | }); 88 | 89 | // Quit when all windows are closed. 90 | app.on('window-all-closed', () => { 91 | // on OS X it is common for applications and their menu bar to stay active until the user quits explicity with Cmd + Q. 92 | if (process.platform !== 'darwin') app.quit(); 93 | }); 94 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | dockure 9 | 10 | 11 |
    12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dockure", 3 | "version": "1.0.0", 4 | "description": "Simplifying the containerization process", 5 | "main": "./electron/main.js", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production electron .", 8 | "build": "NODE_ENV=production webpack", 9 | "dev": "cross-env NODE_ENV=development concurrently \"NODE_ENV=development webpack serve --open\" \"NODE_ENV=development nodemon ./server/server.js\"", 10 | "test-start": "cross-env NODE_ENV=development concurrently 'electron .' 'nodemon server/server.js'", 11 | "test": "jest" 12 | }, 13 | "nodemonConfig": { 14 | "ignore": [ 15 | "build", 16 | "client" 17 | ] 18 | }, 19 | "author": "Alex Zayas, Nathanael Tracy, Liam Talty, Van Nguyen, Hazel Na", 20 | "license": "MIT", 21 | "dependencies": { 22 | "@fortawesome/fontawesome-svg-core": "^1.2.36", 23 | "@fortawesome/free-solid-svg-icons": "^5.15.4", 24 | "@fortawesome/react-fontawesome": "^0.1.15", 25 | "axios": "^0.21.1", 26 | "babelify": "^10.0.0", 27 | "bcryptjs": "^2.4.3", 28 | "body-parser": "^1.19.0", 29 | "codemirror": "^5.62.3", 30 | "cors": "^2.8.5", 31 | "dotenv": "^10.0.0", 32 | "express": "^4.17.1", 33 | "file-saver": "^2.0.5", 34 | "js-yaml": "^4.1.0", 35 | "jsonwebtoken": "^8.5.1", 36 | "jwt-decode": "^3.1.2", 37 | "moment": "^2.29.1", 38 | "node-fetch": "^2.6.1", 39 | "pg": "^8.7.1", 40 | "prop-types": "^15.7.2", 41 | "raw-loader": "^4.0.2", 42 | "react": "^17.0.2", 43 | "react-codemirror2": "^7.2.1", 44 | "react-dom": "^17.0.2", 45 | "react-redux": "^7.2.4", 46 | "react-router-dom": "^5.2.0", 47 | "recharts": "^2.1.2", 48 | "redux": "^4.1.1", 49 | "redux-devtools-extension": "^2.13.9", 50 | "util": "^0.12.4", 51 | "yarn": "^1.22.11" 52 | }, 53 | "devDependencies": { 54 | "@babel/core": "^7.15.0", 55 | "@babel/plugin-transform-runtime": "^7.15.0", 56 | "@babel/preset-env": "^7.15.0", 57 | "@babel/preset-react": "^7.14.5", 58 | "@babel/preset-typescript": "^7.15.0", 59 | "@testing-library/jest-dom": "^5.15.1", 60 | "@testing-library/react": "^12.1.2", 61 | "@testing-library/user-event": "^13.5.0", 62 | "babel-jest": "^27.3.1", 63 | "babel-loader": "^8.2.2", 64 | "concurrently": "^6.2.1", 65 | "cross-env": "^7.0.3", 66 | "css-loader": "^6.2.0", 67 | "electron": "13.1.9", 68 | "html-webpack-plugin": "^5.3.2", 69 | "isomorphic-fetch": "^3.0.0", 70 | "jest": "^27.3.1", 71 | "nodemon": "^2.0.12", 72 | "react-test-renderer": "^17.0.2", 73 | "resolve-url-loader": "^4.0.0", 74 | "sass": "^1.37.5", 75 | "sass-loader": "^12.1.0", 76 | "source-map-loader": "^3.0.0", 77 | "style-loader": "^3.2.1", 78 | "svg-url-loader": "^7.1.1", 79 | "webpack": "^5.50.0", 80 | "webpack-cli": "^4.7.2", 81 | "webpack-dev-server": "^3.11.2" 82 | }, 83 | "jest": { 84 | "testEnvironment": "jsdom" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/assets/prometheus.yaml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | 8 | # Attach these labels to any time series or alerts when communicating with 9 | # external systems (federation, remote storage, Alertmanager). 10 | external_labels: 11 | monitor: 'codelab-monitor' 12 | 13 | # Alertmanager configuration 14 | alerting: 15 | alertmanagers: 16 | - static_configs: 17 | - targets: 18 | # - alertmanager:9093 19 | 20 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 21 | rule_files: 22 | - 'prometheus.rules.yml' 23 | # - "first_rules.yml" 24 | # - "second_rules.yml" 25 | 26 | # A scrape configuration containing exactly one endpoint to scrape: 27 | # Here it's Prometheus itself. 28 | scrape_configs: 29 | # The job name is added as a label `job=` to any timeseries scraped from this config. 30 | - job_name: "prometheus" 31 | 32 | # Override the global default and scrape targets from this job every 5 seconds. 33 | scrape_interval: 5s 34 | 35 | # metrics_path defaults to '/metrics' 36 | # scheme defaults to 'http'. 37 | 38 | static_configs: 39 | - targets: ["localhost:9090", "host.docker.internal:9323"] 40 | 41 | - job_name: 'cadvisor' 42 | 43 | scrape_interval: 10s 44 | 45 | tls_config: 46 | insecure_skip_verify: true 47 | 48 | static_configs: 49 | - targets: 50 | # - 'localhost:9101' 51 | # - 'cadvisor:9101' 52 | - 'host.docker.internal:9101' 53 | labels: 54 | alias: 'cadvisor' 55 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config(); 3 | 4 | const config = { 5 | jwt: { 6 | secretKey: process.env.JWT_SECRET, 7 | expiresInSec: parseInt(process.env.JWT_EXPIRES_SEC), 8 | }, 9 | bcrypt: { 10 | saltRounds: parseInt(process.env.BCRYPT_SALT_ROUNDS), 11 | }, 12 | db: { 13 | host: process.env.DB_HOST, 14 | }, 15 | }; 16 | 17 | module.exports = config; 18 | -------------------------------------------------------------------------------- /server/controllers/authentication.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const config = require('../config'); 3 | 4 | const AUTH_ERROR = { message: 'Authentication Error' }; 5 | 6 | const authentication = async (req, res, next) => { 7 | const authHeader = req.get('Authorization'); 8 | if (!(authHeader && authHeader.startsWith('Bearer '))) { 9 | return res.status(401).json(AUTH_ERROR); 10 | } 11 | 12 | const token = authHeader.split(' ')[1]; 13 | jwt.verify(token, config.jwt.secretKey, (error, decoded) => { 14 | if (error) { 15 | return res.status(401).json(AUTH_ERROR); 16 | } 17 | next(); 18 | }); 19 | }; 20 | 21 | module.exports = authentication; 22 | -------------------------------------------------------------------------------- /server/controllers/cadvisorStart.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const path = require('path'); 3 | 4 | const cadvisorStartController = {}; 5 | 6 | //Standard middleware error handling won't work here 7 | cadvisorStartController.restartCadvisor = async (req, res, next) => { 8 | exec('docker start cadvisor'); 9 | return next(); 10 | } 11 | 12 | cadvisorStartController.startCadvisor = async (req, res, next) => { 13 | if (res.locals.cadvRunning) return next(); 14 | if (process.platform === 'linux' || process.platform === 'win32') { 15 | await exec(`docker run --volume=/sys:/sys:ro --volume=/cgroup:/cgroup:ro --publish=9101:8080 --detach=true --name=cadvisor gcr.io/cadvisor/cadvisor:latest`, (error, stdout, stderr) => {}); 16 | } 17 | else { 18 | await exec(`docker run -d --name=cadvisor -p 9101:8080 --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro --volume=/sys/fs/cgroup:/sys/fs/cgroup:ro --volume=/dev/disk/:/dev/disk:ro raymondmm/cadvisor`, (error, stdout, stderr) => {}); 19 | } 20 | return next(); 21 | } 22 | 23 | module.exports = cadvisorStartController; -------------------------------------------------------------------------------- /server/controllers/dContainer.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const axios = require('axios'); 3 | const util = require('util'); 4 | const conController = {}; 5 | 6 | 7 | conController.getContainers = async(req, res, next) => { 8 | try { 9 | const result = await axios.get('http://localhost:2375/containers/json?all=true') 10 | res.locals.containers = result.data; 11 | return next(); 12 | } catch(err) { 13 | return next(err) 14 | } 15 | } 16 | 17 | conController.getStats = async (req, res, next) => { 18 | const { containerID } = req.body 19 | try { 20 | const result = await axios.get(`http://localhost:2375/containers/${containerID}/stats?stream=false`) 21 | res.locals.data = result.data 22 | return next(); 23 | } catch(err) { 24 | console.log(err) 25 | return next(err) 26 | } 27 | } 28 | 29 | //https://docs.docker.com/engine/api/v1.24/ 30 | conController.startContainer = async (req, res, next) => { 31 | const { containerID } = req.body 32 | 33 | try { 34 | const { status } = await axios.post(`http://localhost:2375/containers/${containerID}/start`) 35 | res.locals.status = status; 36 | return next(); 37 | } catch(err) { 38 | return next(err); 39 | } 40 | } 41 | 42 | conController.stopContainer = async (req, res, next) => { 43 | const { containerID } = req.body; 44 | 45 | try{ 46 | const { status } = await axios.post(`http://localhost:2375/containers/${containerID}/stop`) 47 | res.locals.status = status; 48 | return next(); 49 | } catch(err) { 50 | if(err.response.status === 304) { 51 | res.locals.status = 304; 52 | return next() 53 | } 54 | return next(err); 55 | } 56 | }; 57 | 58 | conController.killContainer = async (req, res, next) => { 59 | const { containerID } = req.body 60 | try { 61 | const { status } = await axios.post(`http://localhost:2375/containers/${containerID}/kill`) 62 | res.locals.status = status; 63 | return next(); 64 | } catch(err) { 65 | console.log('There was an error killing the container: ', err); 66 | return next(err) 67 | } 68 | } 69 | 70 | conController.restartContainer = async (req, res, next) => { 71 | const { containerID } = req.body 72 | 73 | try { 74 | const { status } = await axios.post(`http://localhost:2375/containers/${containerID}/restart?t=5`) 75 | res.locals.status = status; 76 | return next(); 77 | } catch(err) { 78 | console.log('There was an error restarting the container: ', err); 79 | return next(err) 80 | } 81 | } 82 | 83 | conController.pauseContainer = async (req, res, next) => { 84 | const { containerID } = req.body 85 | 86 | try { 87 | const { status } = await axios.post(`http://localhost:2375/containers/${containerID}/pause`) 88 | 89 | res.locals.status = status; 90 | return next(); 91 | } catch(err) { 92 | console.log('There was an error pausing the container: ', err); 93 | return next(err) 94 | } 95 | } 96 | 97 | conController.resumeContainer = async (req, res, next) => { 98 | 99 | const { containerID } = req.body 100 | 101 | 102 | try { 103 | const { status } = await axios.post(`http://localhost:2375/containers/${containerID}/unpause`) 104 | res.locals.status = status; 105 | return next(); 106 | } catch(err) { 107 | console.log(err) 108 | return next(err) 109 | } 110 | } 111 | 112 | conController.removeContainer = async (req, res, next) => { 113 | const { containerID } = req.body 114 | 115 | try { 116 | const { status } = await axios.delete(`http://localhost:2375/containers/${containerID}`) 117 | res.locals.status = status; 118 | return next(); 119 | } catch(err) { 120 | console.log(err) 121 | return next(err) 122 | } 123 | } 124 | 125 | //Standard middleware error handling won't work here 126 | conController.restartSocat = async (req, res, next) => { 127 | exec('docker start socat'); 128 | return next(); 129 | } 130 | 131 | //docker run -d -v /var/run/docker.sock:/var/run/docker.sock --name socat -p 127.0.0.1:2375:2375 bobrik/socat TCP-LISTEN:2375,fork UNIX-CONNECT:/var/run/docker.sock 132 | conController.startSocat = async (req, res, next) => { 133 | if (res.locals.running) return next(); 134 | await exec(`docker run -d -v /var/run/docker.sock:/var/run/docker.sock --name socat -p 127.0.0.1:2375:2375 bobrik/socat TCP-LISTEN:2375,fork UNIX-CONNECT:/var/run/docker.sock`, (error, stdout, stderr) => {}); 135 | return next(); 136 | } 137 | 138 | //throttler for getting containers 139 | conController.throttle = async (req, res, next) => { 140 | let finished = false; 141 | const throttle = () => { 142 | setTimeout(async () => { 143 | await exec('docker ps', (error, stdout, stderr) => { 144 | if (finished) return next(); 145 | if (stdout.includes('prometheus') && stdout.includes('cadvisor') && stdout.includes('socat')) { 146 | finished = true; 147 | throttle() 148 | } 149 | else { 150 | throttle(); 151 | } 152 | }) 153 | }, 200); 154 | } 155 | throttle(); 156 | } 157 | 158 | 159 | module.exports = conController; 160 | 161 | -------------------------------------------------------------------------------- /server/controllers/dImage.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const { exec } = require('child_process'); 3 | const path = require('path'); 4 | 5 | const imageController = {}; 6 | 7 | imageController.getImages = async(req, res, next) => { 8 | try { 9 | const result = await axios.get('http://localhost:2375/images/json?all=true'); 10 | res.locals.images = result.data; 11 | return next(); 12 | } catch(err){ 13 | return next(err); 14 | } 15 | } 16 | 17 | imageController.startImage = (req, res, next) => { 18 | 19 | res.locals.imageID = req.body.imageID 20 | exec(`docker run ${req.body.imageID}`, (error, stdout, stderr) => { 21 | if (error) { 22 | console.log(`error: ${error.message}`); 23 | return next(error); 24 | } 25 | if (stderr) { 26 | console.log(`stderr: ${stderr}`); 27 | return next(stderr); 28 | }; 29 | }); 30 | return next(); 31 | } 32 | 33 | imageController.deleteImage = (req, res, next) => { 34 | res.locals.imageID = req.body.imageID 35 | 36 | exec(`docker image rm ${req.body.imageID}`, (error, stdout, stderr) => { 37 | if (error) { 38 | console.log(`error: ${error.message}`); 39 | return next(error); 40 | } 41 | if (stderr) { 42 | console.log(`stderr: ${stderr}`); 43 | return next(stderr); 44 | }; 45 | }); 46 | return next(); 47 | } 48 | 49 | imageController.pullImage = (req, res, next) => { 50 | 51 | const { imageName } = req.body 52 | 53 | try { 54 | exec(`docker pull ${imageName}`, (error, stdout, stderr) => { 55 | if (error) { 56 | return next(error); 57 | } 58 | if (stderr) { 59 | return next(stderr); 60 | }; 61 | }); 62 | return next(); 63 | } catch(err) { 64 | return next(err) 65 | } 66 | } 67 | 68 | imageController.buildImage = (req, res, next) => { 69 | try { 70 | exec(`docker build -t ${req.body.imageName} ${req.body.path}`, (error, stdout, stderr) => { 71 | if (error) { 72 | return next(error); 73 | } 74 | if (stderr) { 75 | return next(stderr); 76 | }; 77 | }); 78 | 79 | return next(); 80 | } catch(err) { 81 | return next(err) 82 | } 83 | } 84 | 85 | 86 | module.exports = imageController; -------------------------------------------------------------------------------- /server/controllers/isAuth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const config = require('../config'); 3 | const pool = require('../database/dbConnect'); 4 | 5 | const AUTH_ERROR = { message: 'Authentication Error' }; 6 | 7 | const isAuth = async (req, res, next) => { 8 | const authHeader = req.get('Authorization'); 9 | if (!(authHeader && authHeader.startsWith('Bearer '))) { 10 | return res.status(401).json(AUTH_ERROR); 11 | } 12 | 13 | const token = authHeader.split(' ')[1]; 14 | jwt.verify(token, config.jwt.secretKey, async (error, decoded) => { 15 | if (error) { 16 | return res.status(401).json(AUTH_ERROR); 17 | } 18 | 19 | const id = decoded.id; 20 | const params = [id]; 21 | const query = `SELECT id FROM users WHERE id=$1 `; 22 | const { rows } = await pool.query(query, params); 23 | const user = rows[0]; 24 | if (!user) { 25 | return res.status(401).json(AUTH_ERROR); 26 | } 27 | req.userId = user.id; 28 | req.token = token; 29 | next(); 30 | }); 31 | }; 32 | 33 | module.exports = isAuth; 34 | -------------------------------------------------------------------------------- /server/controllers/metricQueries.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const metricQueriesController = {}; 4 | 5 | metricQueriesController.getMetrics = async (req, res, next) => { 6 | try { 7 | const start = res.locals.start; 8 | const end = res.locals.end; 9 | const metrics = req.query.query; 10 | const query = `http://localhost:9090/api/v1/query_range?query=${metrics}&start=${start}&end=${end}&step=15`; 11 | const data = await fetch(query); 12 | const results = await data.json(); 13 | res.locals.values = results.data.result[0].values 14 | return next(); 15 | } catch (error) { 16 | if (error) return next(error); 17 | } 18 | 19 | } 20 | 21 | module.exports = metricQueriesController; -------------------------------------------------------------------------------- /server/controllers/nodeExporter.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | const nodeExporter = {}; 4 | 5 | nodeExporter.start = (req, res, next) => { 6 | if (res.locals.nodeExists === true) return next(); 7 | try { 8 | exec('docker run --name node-exporter -p 9100:9100 prom/node-exporter', (error, stdout, stderr) => { 9 | if (error) { 10 | console.log(`error: ${error.message}`); 11 | return next(error); 12 | } 13 | if (stderr) { 14 | console.log(`stderr: ${stderr}`); 15 | return next(stderr); 16 | }; 17 | }) 18 | } catch (error) { 19 | if (error) { 20 | return next(error); 21 | } 22 | } 23 | return next(); 24 | } 25 | 26 | nodeExporter.check = (req, res, next) => { 27 | let running = false; 28 | let toCheck = ''; 29 | if (res.locals.running !== undefined) toCheck = '-a'; 30 | 31 | exec(`docker ps ${toCheck}`, (error, stdout, stderr) => { 32 | 33 | if (error) { 34 | console.log(`error: ${error.message}`); 35 | return; 36 | } 37 | if (stderr) { 38 | console.log(`stderr: ${stderr}`); 39 | return; 40 | }; 41 | 42 | const target = 'prom/node-exporter'; 43 | for (let i = 0; i < stdout.length && running === false; i++) { 44 | if (stdout[i] === 'p') { 45 | for (let j = 0; j < target.length; j++) { 46 | const targetLetter = target[j]; 47 | const containersLetter = stdout[i]; 48 | if (targetLetter === containersLetter && target.length - 1 === j) { 49 | if (res.locals.running === undefined) { 50 | running = true; 51 | res.locals.nodeExists = true; 52 | } 53 | else { 54 | res.locals.nodeExists = true; 55 | } 56 | 57 | } 58 | else if (targetLetter !== containersLetter) { 59 | break; 60 | } 61 | i++; 62 | } 63 | } 64 | } 65 | res.locals.running = running; 66 | return next(); 67 | }) 68 | } 69 | 70 | nodeExporter.restart = (req, res, next) => { 71 | try { 72 | if (res.locals.nodeExists === true && res.locals.running === false) { 73 | exec('docker restart node-exporter', (error, stdout, stderr) => { 74 | if (error) { 75 | console.log(`error: ${error.message}`); 76 | return; 77 | } 78 | if (stderr) { 79 | console.log(`stderr: ${stderr}`); 80 | return; 81 | }; 82 | }) 83 | } 84 | return next(); 85 | } catch (error) { 86 | if (error) { 87 | return next(error); 88 | } 89 | } 90 | } 91 | 92 | module.exports = nodeExporter; 93 | -------------------------------------------------------------------------------- /server/controllers/promMetrics.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const path = require('path'); 3 | 4 | const promContainerController = {}; 5 | 6 | //Standard middleware error handling won't work here 7 | promContainerController.restartProm = async (req, res, next) => { 8 | exec('docker start prometheus'); 9 | return next(); 10 | } 11 | 12 | promContainerController.startProm = async (req, res, next) => { 13 | if (res.locals.promRunning) return next(); 14 | await exec(`docker run --name prometheus -p 9090:9090 -d -v ${path.join(__dirname, '../assets/prometheus.yaml')}:/etc/prometheus/prometheus.yml prom/prometheus`, (error, stdout, stderr) => {}); 15 | return next(); 16 | } 17 | 18 | 19 | module.exports = promContainerController; 20 | 21 | -------------------------------------------------------------------------------- /server/controllers/timeConversion.js: -------------------------------------------------------------------------------- 1 | 2 | const timeConversionController = {}; 3 | 4 | timeConversionController.unixTime = (req, res, next) => { 5 | try { 6 | let currentTime = new Date().valueOf(); 7 | currentTime = currentTime/1000; 8 | const start = currentTime - (req.query.start * 3600); 9 | res.locals.end = currentTime; 10 | res.locals.start = start; 11 | return next(); 12 | } catch (error) { 13 | return next(error); 14 | } 15 | 16 | } 17 | 18 | module.exports = timeConversionController; -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const bcrypt = require('bcryptjs'); 3 | const pool = require('../database/dbConnect'); 4 | const config = require('../config'); 5 | const userController = {}; 6 | 7 | userController.createUser = async (req, res, next) => { 8 | const { username, password, email } = req.body; 9 | 10 | let hashedPassword; 11 | try { 12 | hashedPassword = await bcrypt.hash(password, config.bcrypt.saltRounds); 13 | } catch (err) { 14 | return next({ 15 | status: 500, 16 | message: err.message, 17 | message2: 'password error', 18 | }); 19 | } 20 | 21 | try { 22 | const params = [username, hashedPassword, email]; 23 | const query = `INSERT INTO users (username, password, email) VALUES ($1,$2,$3) RETURNING id`; 24 | const { rows } = await pool.query(query, params); 25 | res.locals.id = rows[0].id; 26 | 27 | const token = createJwtToken(res.locals.id); 28 | res.locals.token = token; 29 | 30 | return next(); 31 | } catch (err) { 32 | return next({ 33 | status: 500, 34 | message: 'Username already exists', 35 | }); 36 | } 37 | }; 38 | 39 | userController.userLogin = async (req, res, next) => { 40 | const { username, password } = req.body; 41 | let user; 42 | 43 | try { 44 | const params = [username]; 45 | const query = `SELECT * FROM users WHERE username=$1`; 46 | const { rows } = await pool.query(query, params); 47 | if (rows.length < 1) { 48 | return next({ 49 | status: 401, 50 | message: 'Invalid username or password', 51 | }); 52 | } 53 | user = rows[0]; 54 | const compared = await bcrypt.compare(password, user.password); 55 | if (!compared) 56 | return next({ 57 | status: 401, 58 | message: 'The username or password is not valid.', 59 | }); 60 | res.locals.id = user.id; 61 | const token = createJwtToken(res.locals.id); 62 | res.locals.token = token; 63 | return next(); 64 | } catch (err) { 65 | return next({ 66 | status: 500, 67 | message: err.message, 68 | }); 69 | } 70 | }; 71 | 72 | userController.me = async (req, res, next) => { 73 | const id = req.userId; 74 | const params = [id]; 75 | const query = `SELECT * FROM users WHERE id=$1 `; 76 | const { rows } = await pool.query(query, params); 77 | const user = rows[0]; 78 | if (!user) { 79 | return res.status(404).json({ message: 'User not found' }); 80 | } 81 | res.locals.id = user.id; 82 | res.locals.token = req.token; 83 | res.locals.username = user.username; 84 | next(); 85 | }; 86 | 87 | function createJwtToken(id) { 88 | return jwt.sign({ id }, config.jwt.secretKey, { 89 | expiresIn: config.jwt.expiresInSec, 90 | }); 91 | } 92 | 93 | module.exports = userController; 94 | -------------------------------------------------------------------------------- /server/controllers/ymlParser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const yaml = require('js-yaml'); 3 | const path = require('path'); 4 | const { exec } = require('child_process'); 5 | 6 | const yamlParserController = {}; 7 | 8 | 9 | yamlParserController.yamlConfig = (req, res, next) => { 10 | try { 11 | //get default yml 12 | const fileContents = fs.readFileSync(path.resolve(__dirname, '../assets/prometheus.yaml')); 13 | const data = yaml.load(fileContents); 14 | 15 | //update data 16 | const addressesArray = data.scrape_configs[1].static_configs[0].targets; 17 | 18 | for (let i = 0; i < res.locals.ports.length; i++) { 19 | addressesArray.push('host.docker.internal:' + res.locals.ports[i]); 20 | } 21 | 22 | //write data 23 | const yamlStr = yaml.dump(data); 24 | fs.writeFileSync(path.resolve(__dirname, '../assets/promConfigFile.yaml'), yamlStr, 'utf8'); 25 | return next(); 26 | } catch (error) { 27 | return next(error); 28 | } 29 | } 30 | 31 | 32 | yamlParserController.findPorts = (req, res, next) => { 33 | try { 34 | exec("docker ps", (error, stdout, stderr) => { 35 | if (error) { 36 | return `error: ${error.message}`; 37 | } 38 | if (stderr) { 39 | return `stderr: ${stderr}`; 40 | } 41 | const result = stdout.split('\n'); 42 | let values = []; 43 | result.forEach(ele => { 44 | values.push(ele.split(" ").filter(item => item !== "")); 45 | }) 46 | 47 | const keys = values.shift(); 48 | for (let i = 0; i < keys.length; i++) { 49 | if (keys[i][0] === ' ') { 50 | keys[i] = keys[i].slice(1); 51 | } 52 | } 53 | 54 | values = values.map(function (element) { 55 | const obj = {}; 56 | keys.forEach(function (key, index) { 57 | if(element.length) { 58 | obj[key] = element[index]; 59 | } 60 | }); 61 | return obj; 62 | }); 63 | if(JSON.stringify(values[values.length - 1]) === '{}') values.pop(); 64 | res.locals.unparsedContainers = values; 65 | return next(); 66 | }); 67 | } catch (error) { 68 | return next(error); 69 | } 70 | } 71 | 72 | 73 | yamlParserController.portParser = (req, res, next) => { 74 | const values = res.locals.unparsedContainers; 75 | const ports = []; 76 | 77 | for(let i = 0; i < values.length; i += 1) { 78 | if(values[i]['IMAGE'] !== ' prom/prometheus') { 79 | ports.push(values[i]['PORTS']); 80 | } 81 | } 82 | 83 | let parsedPort = ""; 84 | for(let i = 0; i < ports.length; i += 1){ 85 | let foundColon = false; 86 | let foundDash = false; 87 | let index = 0; 88 | let indexOfString = ports[i][index]; 89 | 90 | while(!foundDash) { 91 | if( foundColon === true){ 92 | parsedPort += indexOfString; 93 | } 94 | if(indexOfString === ':'){ 95 | foundColon = true; 96 | } 97 | indexOfString = ports[i][++index]; 98 | if(indexOfString === '-'){ 99 | foundDash = true; 100 | } 101 | } 102 | ports[i] = parsedPort; 103 | parsedPort = '' 104 | } 105 | res.locals.ports = ports; 106 | return next(); 107 | } 108 | 109 | 110 | module.exports = yamlParserController; 111 | -------------------------------------------------------------------------------- /server/database/dbConnect.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | const config = require('../config'); 3 | 4 | const PG_URI = config.db.host; 5 | 6 | // //CREATE A NEW POOL HERE USING THAT CONNECTION STRING ABOVE 7 | 8 | const pool = new Pool({ 9 | connectionString: PG_URI, 10 | }); 11 | 12 | const createUserTable = ` 13 | CREATE TABLE IF NOT EXISTS users ( 14 | id SERIAL PRIMARY KEY, 15 | username VARCHAR (50) UNIQUE NOT NULL, 16 | password VARCHAR (150) NOT NULL, 17 | email VARCHAR (50) UNIQUE NOT NULL 18 | )`; 19 | 20 | 21 | pool.query(createUserTable, (err, res) => { 22 | if (err) { 23 | return err; 24 | } 25 | }); 26 | 27 | module.exports = pool; 28 | -------------------------------------------------------------------------------- /server/database/setup.sql: -------------------------------------------------------------------------------- 1 | /* 2 | These are the tables required to run our app. 3 | Please create a separate database for these tables inside of your DB, e.g. CREATE DATABASE wobble_chat; 4 | Then 5 | */ 6 | 7 | -- CREATE TABLE IF NOT EXISTS users ( 8 | -- id SERIAL PRIMARY KEY NOT NULL, 9 | -- username varchar(50) UNIQUE NOT NULL, 10 | -- password varchar(50) NOT NULL, 11 | -- email varchar(100) UNIQUE NOT NULL, 12 | -- -- isLoggedIn boolean DEFAULT true 13 | -- ); 14 | 15 | -- CREATE TABLE IF NOT EXISTS `user` ( 16 | -- `id` SERIAL NOT NULL, 17 | -- `username` varchar(50) NOT NULL, 18 | -- `password` varchar(50) NOT NULL, 19 | -- `email` varchar(100) NOT NULL, 20 | -- PRIMARY KEY (`id`), 21 | -- UNIQUE KEY `username` (`username`) 22 | -- UNIQUE KEY `email` (`email`) 23 | -- -- isLoggedIn boolean DEFAULT true 24 | -- ); 25 | 26 | -- CREATE TABLE IF NOT EXISTS `user` ( 27 | -- `id` SERIAL NOT NULL, 28 | -- `username` varchar(50) NOT NULL, 29 | -- `password` varchar(50) NOT NULL, 30 | -- `email` varchar(100) NOT NULL, 31 | -- PRIMARY KEY (`id`), 32 | -- UNIQUE KEY `id_UNIQUE` (`id`), 33 | -- UNIQUE KEY `username` (`username`) 34 | -- UNIQUE KEY `email` (`email`) 35 | -- -- isLoggedIn boolean DEFAULT true 36 | -- ); 37 | 38 | -- CREATE TABLE IF NOT EXISTS users ( 39 | -- id SERIAL PRIMARY KEY, 40 | -- username varchar(50) UNIQUE, 41 | -- password varchar(100), 42 | -- isLoggedIn boolean DEFAULT true 43 | -- ); 44 | 45 | -- CREATE TABLE IF NOT EXISTS accounts ( 46 | -- user_id serial PRIMARY KEY, 47 | -- username VARCHAR ( 50 ) UNIQUE NOT NULL, 48 | -- password VARCHAR ( 50 ) NOT NULL, 49 | -- email VARCHAR ( 255 ) UNIQUE NOT NULL, 50 | -- ); 51 | 52 | -- CREATE TABLE IF NOT EXISTS questions ( 53 | -- id SERIAL PRIMARY KEY, 54 | -- title varchar(500) NOT NULL, 55 | -- description text NOT NULL, 56 | -- url varchar(100) UNIQUE NOT NULL, 57 | -- isAnswered boolean DEFAULT false NOT NULL, 58 | -- creator integer REFERENCES users(id) NOT NULL, 59 | -- /* Can you do a foreign key reference of a boolean in another table? The boolean is not a key... */ 60 | -- isOpen boolean DEFAULT false NOT NULL 61 | -- ); 62 | -- CREATE TABLE IF NOT EXISTS messages ( 63 | -- id SERIAL PRIMARY KEY, 64 | -- dateCreated date NOT NULL, 65 | -- questionId integer REFERENCES questions(id), 66 | -- content text NOT NULL, 67 | -- senderid varchar, 68 | -- ownedbycurrentuser boolean, 69 | -- body text, 70 | -- num integer 71 | -- ); -------------------------------------------------------------------------------- /server/routers/dContainer.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const conController = require('../controllers/dContainer.js'); 3 | const containerRouter = express.Router(); 4 | const promContainerController = require('../controllers/promMetrics'); 5 | const cadvisorStartController = require('../controllers/cadvisorStart'); 6 | const authentication = require('../controllers/authentication'); 7 | 8 | containerRouter.get( 9 | '/', 10 | promContainerController.restartProm, 11 | promContainerController.startProm, 12 | cadvisorStartController.restartCadvisor, 13 | cadvisorStartController.startCadvisor, 14 | conController.restartSocat, 15 | conController.startSocat, 16 | conController.throttle, 17 | (req, res) => { 18 | return res.status(200).send('It worked'); 19 | } 20 | ); 21 | 22 | containerRouter.get( 23 | '/containers', 24 | authentication, 25 | conController.getContainers, 26 | (req, res) => { 27 | const result = res.locals.containers; 28 | return res.status(200).send(result); 29 | } 30 | ); 31 | 32 | containerRouter.post('/stats', conController.getStats, (req, res) => { 33 | const result = res.locals.data; 34 | return res.status(200).json(result); 35 | }); 36 | 37 | containerRouter.post( 38 | '/start', 39 | authentication, 40 | conController.startContainer, 41 | (req, res) => { 42 | return res.sendStatus(res.locals.status); 43 | } 44 | ); 45 | 46 | containerRouter.post( 47 | '/stop', 48 | authentication, 49 | conController.stopContainer, 50 | (req, res) => { 51 | return res.sendStatus(res.locals.status); 52 | } 53 | ); 54 | 55 | containerRouter.post( 56 | '/kill', 57 | authentication, 58 | conController.killContainer, 59 | (req, res) => { 60 | return res.sendStatus(res.locals.status); 61 | } 62 | ); 63 | 64 | containerRouter.post( 65 | '/restart', 66 | authentication, 67 | conController.restartContainer, 68 | (req, res) => { 69 | return res.sendStatus(res.locals.status); 70 | } 71 | ); 72 | 73 | containerRouter.post( 74 | '/pause', 75 | authentication, 76 | conController.pauseContainer, 77 | (req, res) => { 78 | return res.sendStatus(res.locals.status); 79 | } 80 | ); 81 | 82 | containerRouter.post( 83 | '/resume', 84 | authentication, 85 | conController.resumeContainer, 86 | (req, res) => { 87 | return res.sendStatus(res.locals.status); 88 | } 89 | ); 90 | 91 | containerRouter.post( 92 | '/remove', 93 | authentication, 94 | conController.removeContainer, 95 | (req, res) => { 96 | return res.sendStatus(res.locals.status); 97 | } 98 | ); 99 | 100 | module.exports = containerRouter; 101 | -------------------------------------------------------------------------------- /server/routers/dImage.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const imageController = require('../controllers/dImage.js'); 3 | const imageRouter = express.Router(); 4 | const authentication = require('../controllers/authentication'); 5 | 6 | imageRouter.get('/', authentication, imageController.getImages, (req, res) => { 7 | const result = res.locals.images; 8 | return res.status(200).send(result); 9 | }); 10 | 11 | imageRouter.post( 12 | '/start', 13 | authentication, 14 | imageController.startImage, 15 | (req, res) => { 16 | return res.status(200).send('running'); 17 | } 18 | ); 19 | 20 | imageRouter.post( 21 | '/delete', 22 | authentication, 23 | imageController.deleteImage, 24 | (req, res) => { 25 | return res.status(200).send('deleted'); 26 | } 27 | ); 28 | 29 | imageRouter.post( 30 | '/pull', 31 | authentication, 32 | imageController.pullImage, 33 | (req, res) => { 34 | return res.status(200); 35 | } 36 | ); 37 | 38 | imageRouter.post( 39 | '/build', 40 | authentication, 41 | imageController.buildImage, 42 | (req, res) => { 43 | return res.status(200); 44 | } 45 | ); 46 | 47 | module.exports = imageRouter; 48 | -------------------------------------------------------------------------------- /server/routers/promMetrics.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const promContainerController = require('../controllers/promMetrics'); 3 | const yamlParserController = require('../controllers/ymlParser'); 4 | const metricQueriesController = require('../controllers/metricQueries'); 5 | const timeConversionController = require('../controllers/timeConversion'); 6 | const cadvisorStartController = require('../controllers/cadvisorStart'); 7 | const nodeExporter = require('../controllers/nodeExporter'); 8 | const promMetricsRouter = express.Router(); 9 | 10 | 11 | promMetricsRouter.get('/promStart', 12 | promContainerController.restartProm, 13 | promContainerController.startProm, 14 | (req, res) => { 15 | return res.status(200).send('it worked!'); 16 | } 17 | ); 18 | 19 | 20 | promMetricsRouter.get('/cadvisorStart', 21 | cadvisorStartController.restartCadvisor, 22 | cadvisorStartController.startCadvisor, 23 | (req, res) => { 24 | return res.status(200).send('it worked!'); 25 | } 26 | ); 27 | 28 | promMetricsRouter.get('/', 29 | timeConversionController.unixTime, 30 | metricQueriesController.getMetrics, 31 | (req, res) => { 32 | return res.status(200).json(res.locals.values); 33 | } 34 | ) 35 | 36 | //route for node-exporter 37 | promMetricsRouter.get('/node-exporter', 38 | 39 | nodeExporter.check, 40 | nodeExporter.check, 41 | nodeExporter.restart, 42 | nodeExporter.start, 43 | (req, res) => { 44 | return res.status(200).send('Node-exporter worked'); 45 | } 46 | ) 47 | 48 | module.exports = promMetricsRouter; -------------------------------------------------------------------------------- /server/routers/user.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const conController = require('../controllers/dContainer.js'); 3 | const userController = require('../controllers/userController.js'); 4 | const isAuth = require('../controllers/isAuth'); 5 | 6 | 7 | const userRouter = Router(); 8 | 9 | //route handler 10 | userRouter.post('/signup', userController.createUser, (req, res) => { 11 | res.status(200).send({ id: res.locals.id, token: res.locals.token }); 12 | }); 13 | 14 | userRouter.post('/login', 15 | userController.userLogin, 16 | (req, res) => { 17 | res.status(200).send({ 18 | id: res.locals.id, 19 | token: res.locals.token, 20 | }); 21 | }); 22 | 23 | userRouter.get('/me', isAuth, userController.me, 24 | (req, res) => { 25 | res 26 | .status(200) 27 | .send({ 28 | id: res.locals.id, 29 | token: res.locals.token, 30 | username: res.locals.username, 31 | }) 32 | }); 33 | 34 | userRouter.get('/checkme', isAuth, userController.me, (req, res) => { 35 | res 36 | .status(200) 37 | .send({ 38 | id: res.locals.id, 39 | token: res.locals.token, 40 | username: res.locals.username, 41 | }); 42 | }); 43 | 44 | module.exports = userRouter; 45 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | const PORT = 3000; 5 | const cors = require('cors'); 6 | 7 | //import routers 8 | const containerRouter = require('./routers/dContainer.js'); 9 | const promMetricsRouter = require('./routers/promMetrics.js'); 10 | const imageRouter = require('./routers/dImage.js'); 11 | const userRouter = require('./routers/user.js'); 12 | 13 | app.use(express.json()); 14 | app.use(express.urlencoded({ extended: true })); 15 | app.use(cors()); 16 | 17 | app.get('/', (req, res) => { 18 | return res.sendStatus(200); 19 | }); 20 | 21 | //routing routers 22 | app.use('/api/user', userRouter); 23 | app.use('/api/containers', containerRouter); 24 | app.use('/api/images', imageRouter); 25 | 26 | //routing for prometheus metrics 27 | app.use('/api/metrics', promMetricsRouter); 28 | 29 | //unknown path handler 30 | app.use('*', (req, res) => { 31 | res.status(404).send('That is an unknown url'); 32 | }); 33 | 34 | //global error handlings 35 | app.use((err, req, res, next) => { 36 | const defaultErr = { 37 | log: 'Express error handler caught an unknown middleware error', 38 | status: 500, 39 | message: { err: 'An error occurred' }, 40 | }; 41 | const errorObj = Object.assign(defaultErr, err); 42 | console.log(errorObj.log); 43 | res.status(errorObj.status).json(errorObj.message); 44 | }); 45 | 46 | app.listen(PORT, () => console.log(`Listening on port ${PORT}`)); 47 | 48 | module.exports = { app }; 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": [ 6 | "dom", 7 | "es2015", 8 | "es2016", 9 | "es2017" 10 | ], 11 | "allowJs": true, 12 | "jsx": "react", 13 | "sourceMap": true, 14 | "outDir": "./dist", 15 | "strict": true, 16 | "esModuleInterop": true, 17 | } 18 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV, 6 | entry: './client/index.js', 7 | target: 'web', 8 | // "electron-renderer", 9 | devtool: 'inline-source-map', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|jsx|ts|tsx)$/, 14 | exclude: /node_modules/, 15 | loader: 'babel-loader', 16 | options: { 17 | presets: ['@babel/preset-env', '@babel/preset-react'], 18 | plugins: [ 19 | '@babel/plugin-transform-runtime', 20 | '@babel/transform-async-to-generator', 21 | ], 22 | }, 23 | }, 24 | { 25 | test: /\.s[ac]ss$/i, 26 | exclude: /node_modules/, 27 | use: [ 28 | 'style-loader', 29 | 'css-loader', 30 | 'resolve-url-loader', 31 | 'sass-loader', 32 | ], 33 | }, 34 | { 35 | test: /\.svg$/, 36 | use: [ 37 | { 38 | loader: 'svg-url-loader', 39 | options: { 40 | limit: 10000, 41 | }, 42 | }, 43 | ], 44 | }, 45 | { 46 | test: /\.css$/, 47 | use: [ 48 | 'style-loader', 49 | { 50 | loader: 'css-loader', 51 | options: { 52 | importLoaders: 1, 53 | modules: true, 54 | }, 55 | }, 56 | ], 57 | include: /\.module\.css$/, 58 | }, 59 | { 60 | test: /\.css$/, 61 | use: ['style-loader', 'css-loader'], 62 | exclude: /\.module\.css$/, 63 | }, 64 | ], 65 | }, 66 | devServer: { 67 | historyApiFallback: true, 68 | compress: true, 69 | hot: true, 70 | port: 8080, 71 | publicPath: '/build/', 72 | proxy: { 73 | '/api': 'http://localhost:3000', 74 | }, 75 | }, 76 | output: { 77 | path: path.resolve(__dirname, 'build'), 78 | filename: 'bundle.js', 79 | }, 80 | resolve: { 81 | extensions: ['.tsx', '.ts', '.js', '.jsx'], 82 | mainFields: ['main', 'module', 'browser'], 83 | }, 84 | plugins: [ 85 | new HtmlWebpackPlugin({ 86 | template: './index.html', 87 | }), 88 | ], 89 | }; 90 | --------------------------------------------------------------------------------