├── .dockerignore ├── .github └── FUNDING.yml ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── app.js ├── backend ├── .gitignore ├── README.md ├── controllers │ ├── CleanUpController.js │ ├── ContainerController.js │ ├── DefaultController.js │ ├── GenericCommandController.js │ ├── GroupController.js │ └── ImageController.js ├── index.js ├── package.json ├── utilities │ ├── db.js │ ├── lightContainerDetail.js │ ├── lightImageDetail.js │ └── terminal.js └── web │ ├── asset-manifest.json │ ├── docker-icon.png │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ ├── precache-manifest.adb95cf9ceba078f9a2aef310d78b377.js │ ├── robots.txt │ ├── service-worker.js │ └── static │ ├── css │ ├── main.ab6a2c25.chunk.css │ └── main.ab6a2c25.chunk.css.map │ └── js │ ├── 2.f3d6def3.chunk.js │ ├── 2.f3d6def3.chunk.js.map │ ├── main.5147f23d.chunk.js │ ├── main.5147f23d.chunk.js.map │ ├── runtime~main.e38743dc.js │ └── runtime~main.e38743dc.js.map ├── client ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── docker-icon.png │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── components │ ├── Loader.js │ ├── LogSideSheet.js │ ├── NavBar.js │ ├── SecondaryNavBar.js │ ├── cleanup │ │ ├── cleanUpInfo.js │ │ └── cleanupSubNav.js │ ├── container │ │ ├── card.js │ │ ├── deleteModal.js │ │ ├── lists.js │ │ ├── restartButton.js │ │ ├── selector.js │ │ ├── stat.js │ │ ├── style │ │ │ ├── card.css │ │ │ └── modal.css │ │ └── switch.js │ ├── createdAt.js │ ├── groups │ │ ├── GroupCard.js │ │ ├── GroupDeleteButton.js │ │ ├── GroupSwitch.js │ │ ├── GroupsList.js │ │ ├── NewGroupForm.js │ │ └── style │ │ │ └── GroupCard.css │ └── image │ │ ├── imageCard.js │ │ └── imageLists.js │ ├── index.css │ ├── index.js │ ├── pages │ ├── cleanup.page.js │ ├── container.page.js │ └── image.page.js │ ├── routes.js │ ├── serviceWorker.js │ ├── store │ ├── actions │ │ ├── cleanUp.action.js │ │ ├── container.action.js │ │ ├── groups.action.js │ │ ├── image.action.js │ │ └── stats.action.js │ ├── index.js │ ├── reducers │ │ ├── cleanUp.reducer.js │ │ ├── container.reducer.js │ │ ├── groups.reducer.js │ │ ├── image.reducer.js │ │ ├── index.js │ │ └── stats.reducer.js │ └── schema │ │ ├── cleanup.schema.js │ │ ├── container.schema.js │ │ ├── groups.schema.js │ │ ├── image.schema.js │ │ ├── index.js │ │ └── stats.schema.js │ └── utilities │ └── request.js └── docker-compose.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/data.db 3 | .idea 4 | .DS_Store -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rakibtg 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Node.js 18 (LTS) as the base image for stability and ES module support 2 | FROM node:18-alpine 3 | 4 | # Set the working directory 5 | WORKDIR /src 6 | 7 | # Install Python, pip, and build tools in one RUN command 8 | RUN apk add --no-cache python3 py3-pip build-base docker-cli 9 | 10 | # Copy application files to the container 11 | COPY ./backend /src/backend 12 | COPY ./client /src/client 13 | COPY ./app.js /src/app.js 14 | 15 | # Install backend dependencies 16 | RUN cd /src/backend && npm install 17 | 18 | # Expose the application port 19 | EXPOSE 3230 20 | 21 | # Run the application 22 | CMD ["node", "/src/app.js"] 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker-compose build 3 | 4 | up: 5 | docker-compose up -d 6 | 7 | up-non-daemon: 8 | docker-compose up 9 | 10 | start: 11 | docker-compose start 12 | 13 | stop: 14 | docker-compose stop 15 | 16 | restart: 17 | docker-compose stop && docker-compose start 18 | 19 | run-without-compose: 20 | docker run -p 3230:3230 -v /usr/local/bin/docker:/usr/local/bin/docker -v /var/run/docker.sock:/var/run/docker.sock docker-web-gui 21 | 22 | build-without-compose: 23 | docker build . -t docker-web-gui -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Dashboard 2 | 3 | ## A simple GUI interface for Docker Containers 4 | 5 |

6 | Docker Web Interface Project - A simple GUI interface for Docker 7 |

8 | 9 | |


Explore containers log

|


List of groups

| 10 | | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | 11 | 12 | |


List of groups

|


List of images

|


Disk cleanup's

| 13 | | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | 14 | 15 | ## Features 16 | 17 | - Instantly start/stop, restart, delete and see the logs of a docker container. 18 | - Filter containers by their running status. 19 | - Create groups of docker container. 20 | - Bulk action on container based on group. 21 | - Live system consumption stat for active docker containers. 22 | - Run or delete an image. 23 | - Prune Docker images. 24 | - Prune Docker containers. 25 | - Prune Docker volumes. 26 | - Prune Docker systems. 27 | - No need to use the terminal for common tasks. 28 | 29 | ## Start the app 30 | 31 | Before you follow below steps to start the app, make sure you have `node` and `npm` installed in your system. 32 | 33 | - Clone the repository 34 | ``` 35 | git clone git@github.com:rakibtg/docker-web-gui.git 36 | ``` 37 | - Change directory 38 | ``` 39 | cd ./docker-web-gui 40 | ``` 41 | - Run `app.js`, it will automatically install all the [node modules](https://github.com/rakibtg/docker-web-gui/blob/master/backend/package.json) for you if not installed already. 42 | ``` 43 | node app.js 44 | ``` 45 | - Now visit http://localhost:3230/ 46 | 47 | ## Using Docker 48 | 49 | You can run this application through a docker container, but it only works in **MacOS**. You can use that with/without [**`docker compose`**](https://docs.docker.com/compose/). 50 | Also, the application will be exposed at port http://localhost:3230. 51 | 52 | ### Without Docker Compose 53 | 54 | If you don't have a docker compose, then you can use the following commands: 55 | 56 | - To build the image: 57 | ``` 58 | docker build . -t docker-web-gui 59 | ``` 60 | - To run the image: 61 | ``` 62 | docker run -p 3230:3230 -v /usr/local/bin/docker:/usr/local/bin/docker -v /var/run/docker.sock:/var/run/docker.sock docker-web-gui 63 | ``` 64 | 65 | ### With Docker Compose 66 | 67 | If you already docker compose installed, then simply do this: 68 | 69 | ``` 70 | docker-compose build 71 | docker-compose up 72 | ``` 73 | 74 | ### Docker Based Commands 75 | 76 | A `Makefile` has been included with this repo. It has following commands: 77 | 78 | 1. `make up` to build the image and starting `docker-web-gui` container. 79 | 2. `make build` to build the image. 80 | 3. `make start` to start containers if application has been up already. 81 | 4. `make stop` to stop application. 82 | 5. `make restart` to restart application. 83 | 6. `make build-without-compose` to build the application without _docker compose_. 84 | 7. `make run-without-compose` to run the application without _docker compose_. 85 | 86 | # Documentations 87 | 88 | - [Backend API](https://github.com/rakibtg/docker-web-gui/tree/master/backend) 89 | - [Client](https://github.com/rakibtg/docker-web-gui/tree/master/client) 90 | 91 | Developed by [Hasan](https://twitter.com/rakibtg) and [contributors](https://github.com/rakibtg/docker-web-gui/graphs/contributors). 92 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const { safeTerminal } = require("./backend/utilities/terminal"); 4 | const port = 3230; 5 | 6 | async function app() { 7 | console.clear(); 8 | 9 | const BACKEND = path.resolve(__dirname + "/backend/"); 10 | const NODE_MODULES = BACKEND + "/node_modules"; 11 | 12 | if (!fs.existsSync(NODE_MODULES)) { 13 | console.log( 14 | "🚀 Please wait while we install all the dependencies for you...\n" 15 | ); 16 | await safeTerminal.installModules(BACKEND); 17 | console.log("🎉 All dependencies added successfully!"); 18 | } 19 | 20 | setTimeout(() => { 21 | console.log(`✨ Visit http://localhost:${port} to use Docker Web GUI`); 22 | }, 1500); 23 | await safeTerminal.serve(BACKEND); 24 | } 25 | 26 | app(); 27 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | package.json.lock 4 | package-lock.json 5 | data.db -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ## Backend API 2 | 3 | - Get a list of containers 4 | - Endpoint: `/api/container/fetch?status=active` 5 | - Method: `GET` 6 | - Query Params: 7 | - `status` 8 | - Value: `all`, `active`, `stopped` 9 | - default: `active` 10 | - Response: A list of containers 11 | ```JSON 12 | [ 13 | { 14 | "Id": "19ea7e796993c1b3486ffa4207994ef7e9cf7844072c6760970375b89e96d45c", 15 | "shortId": "19ea7e796993", 16 | "Created": "2019-08-27T15:44:09.8723696Z", 17 | "State": { 18 | "Status": "exited", 19 | "Running": false, 20 | "Paused": false, 21 | "Restarting": false, 22 | "OOMKilled": false, 23 | "Dead": false, 24 | "Pid": 0, 25 | "ExitCode": 100, 26 | "Error": "", 27 | "StartedAt": "2019-08-27T15:44:10.5869722Z", 28 | "FinishedAt": "2019-08-27T15:44:29.0294127Z" 29 | }, 30 | "Name": "/goofy_antonelli" 31 | }, 32 | {...}, 33 | {...} 34 | ] 35 | ``` 36 | 37 | - Get a container by ID 38 | - Endpoint: `/api/container/fetchById?container=container-id` 39 | - Method: `GET` 40 | - Query Params: 41 | - `container` 42 | - Value: `container-id` ex: 3da3ad7b90e3 43 | - Response: An object contained information's about the container 44 | ```JSON 45 | { 46 | "Id": "3da3ad7b90e321fbf0fd2466d2555a7092c0642e7ad07fbe5d623fa0c6f65ada", 47 | "shortId": "3da3ad7b90e3", 48 | "Created": "2019-08-27T16:11:57.4812983Z", 49 | "State": { 50 | "Status": "running", 51 | "Running": true, 52 | "Paused": false, 53 | "Restarting": false, 54 | "OOMKilled": false, 55 | "Dead": false, 56 | "Pid": 2660, 57 | "ExitCode": 0, 58 | "Error": "", 59 | "StartedAt": "2019-08-31T04:25:12.807509894Z", 60 | "FinishedAt": "2019-08-31T04:25:07.825047009Z" 61 | }, 62 | "Name": "/dinky-dank-docker_web_1" 63 | } 64 | ``` 65 | 66 | - Run generic command to a specific container 67 | - Endpoint: `/api/container/command?container=container-id&command=start` 68 | - Method: `GET` 69 | - Query Params: 70 | - `container` 71 | - Value: `container-id` ex: 3da3ad7b90e3 72 | - `command` 73 | - Value: `start`, `stop`, `restart` etc 74 | - Response: It might return the container id most of the time. It could be different based on the command. 75 | ``` 76 | "3da3ad7b90e3" 77 | ``` 78 | 79 | - Get system consumption stats of all active containers 80 | - Endpoint: `/api/container/stats` 81 | - Method: `GET` 82 | - Response: List of status for active containers 83 | ```JSON 84 | [ 85 | { 86 | "id": "3da3ad7b90e3", 87 | "cpu_percentage": "0.01%", 88 | "memory_usage": "9.77MiB / 1.952GiB", 89 | "network_io": "998B / 0B" 90 | }, 91 | {...}, 92 | {...} 93 | ] 94 | ``` 95 | 96 | - Get text log of a container 97 | - Endpoint: `/api/container/logs?container=container-id` 98 | - Method: `GET` 99 | - Query Params: 100 | - `container` 101 | - Value: `container-id` ex: 3da3ad7b90e3 102 | - Response: String with special characters like new lines, tabs etc 103 | ``` 104 | "172.18.0.1 - - 105 | [27/Aug/2019:16:13:01 +0000] \"POST /api/v1/actions/upload/ HTTP/1.1\" 200 1357 \"-\" \"insomnia/6.6.2\"\n172.18.0.1 - - 106 | [27/Aug/2019:16:28:36 +0000] \"POST /api/v1/actions/upload/ HTTP/1.1\" 200 1522 \"-\" \"insomnia/6.6.2\"\n172.18.0.1 - - 107 | [27/Aug/2019:16:31:38 +0000] \"POST /api/v1/actions/upload/ HTTP/1.1\" 200 1357 \"-\" \"insomnia/ 108 | ... 109 | ... 110 | ``` 111 | 112 | - Get a list of images 113 | - Endpoint: `/api/image/fetch` 114 | - Method: `GET` 115 | - Query Params: `None` 116 | - Response: Array 117 | ```JSON 118 | [ 119 | { 120 | "ID": "8f60bf1d0f34", 121 | "CreatedSince": "2 weeks ago", 122 | "Size": "470MB", 123 | "VirtualSize": "470MB", 124 | "Repository": "gsk_kubernets" 125 | }, 126 | { 127 | "ID": "cea865d1a9a0", 128 | "CreatedSince": "2 weeks ago", 129 | "Size": "470MB", 130 | "VirtualSize": "470MB", 131 | "Repository": "docker_web" 132 | }, 133 | {...}, 134 | {...} 135 | ] 136 | ``` 137 | 138 | - Run generic command to a specific image 139 | - Endpoint: `/api/image/command?image=image-id&command=run` 140 | - Method: `GET` 141 | - Query Params: 142 | - `image` 143 | - Value: `image-id` ex: 3da3ad7b90e3 144 | - `command` 145 | - Value: `run`, `rmi` etc 146 | - Response: It might return the image id most of the time. It could be different based on the command. 147 | ``` 148 | "3da3ad7b90e3" 149 | ``` 150 | 151 | - Get a list of container groups 152 | - Endpoint: `/api/groups` 153 | - Method: `GET` 154 | - Query Params: `None` 155 | - Response: List of groups as object. 156 | ``` 157 | { 158 | "id": 13, 159 | "name": "Alien Ship Project", 160 | "containers_id": "[\"1bb3b55b3202\",\"171be371c488\",\"280f85e27167\"]", 161 | "created_at": "2019-09-18 18:01:09", 162 | "updated_at": "2019-09-18 18:01:09" 163 | }, 164 | {...}, 165 | {...}, 166 | ``` 167 | 168 | - Create a container 169 | - Endpoint: `/api/groups` 170 | - Method: `POST` 171 | - Query Params: 172 | - `name` 173 | - Value: 'my group name' 174 | - `containers` 175 | - Value: `['4232ewdw', 'sdsdw24343']` 176 | 177 | - Delete a container 178 | - Endpoint: `/api/groups` 179 | - Method: `DELETE` 180 | - Query Params: 181 | - `id` 182 | - Value: `23` 183 | -------------------------------------------------------------------------------- /backend/controllers/CleanUpController.js: -------------------------------------------------------------------------------- 1 | const { safeTerminal } = require("../utilities/terminal"); 2 | 3 | exports.command = async (req, res, next) => { 4 | const pruneType = req.query.type; 5 | try { 6 | const cmdData = await safeTerminal.prune(pruneType); 7 | res.json(cmdData); 8 | } catch (error) { 9 | next(error); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/controllers/ContainerController.js: -------------------------------------------------------------------------------- 1 | const { safeTerminal } = require("../utilities/terminal"); 2 | const { lightContainerDetail } = require("../utilities/lightContainerDetail"); 3 | 4 | exports.fetch = async (req, res) => { 5 | const status = req.query.status ? req.query.status : "active"; 6 | const rawContainersFromCmd = await safeTerminal.allContainers(); 7 | const containers = rawContainersFromCmd 8 | .split("\n") 9 | .map((container) => container.trim()) 10 | .filter((container) => container !== ""); 11 | let results = []; 12 | await Promise.all( 13 | containers.map(async (container) => { 14 | const inspectedContainer = await safeTerminal.inspectContainer(container); 15 | const tintContainer = JSON.parse(inspectedContainer)[0]; 16 | if (status === "active") { 17 | if (tintContainer.State.Running === true) 18 | results.push(lightContainerDetail(container, tintContainer)); 19 | } else if (status === "all") { 20 | results.push(lightContainerDetail(container, tintContainer)); 21 | } else if (status === "stopped") { 22 | if (tintContainer.State.Running !== true) 23 | results.push(lightContainerDetail(container, tintContainer)); 24 | } 25 | }) 26 | ); 27 | res.json(results.sort((a, b) => (a.Name > b.Name ? 1 : -1))); 28 | }; 29 | 30 | exports.fetchById = async (req, res) => { 31 | const containerID = req.query.container; 32 | const containerInspect = await safeTerminal.inspectContainer(containerID); 33 | const container = lightContainerDetail( 34 | containerID, 35 | JSON.parse(containerInspect)[0] 36 | ); 37 | res.json(container); 38 | }; 39 | 40 | exports.command = async (req, res, next) => { 41 | const containerID = req.query.container; 42 | const command = req.query.command; 43 | try { 44 | const cmdData = await safeTerminal.generic(command, containerID); 45 | res.json(cmdData.replace("\n", "")); 46 | } catch (error) { 47 | next(error); 48 | } 49 | }; 50 | 51 | exports.logs = async (req, res) => { 52 | const containerID = req.query.container; 53 | const data = await safeTerminal.logs(containerID); 54 | res.json(data); 55 | }; 56 | 57 | exports.stats = async (req, res) => { 58 | const cmdStats = await safeTerminal.stats(); 59 | const statsArray = cmdStats 60 | .split("\n") 61 | .filter((container) => container !== "") 62 | .map((stat) => JSON.parse(stat)); 63 | res.json(statsArray); 64 | }; 65 | -------------------------------------------------------------------------------- /backend/controllers/DefaultController.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | exports.DefaultController = (req, res) => { 4 | res.sendFile(path.join(__dirname + '/../web/index.html')) 5 | } -------------------------------------------------------------------------------- /backend/controllers/GenericCommandController.js: -------------------------------------------------------------------------------- 1 | const { safeTerminal } = require("../utilities/terminal"); 2 | 3 | exports.GenericCommandController = async (req, res) => { 4 | const output = await safeTerminal.containerLs(); 5 | const filtered = output.replace(/}\s*{/g, "},{"); 6 | res.json(filtered); 7 | }; 8 | -------------------------------------------------------------------------------- /backend/controllers/GroupController.js: -------------------------------------------------------------------------------- 1 | const db = require('../utilities/db') 2 | 3 | exports.create = async (req, res) => { 4 | 5 | const { 6 | name, containers 7 | } = req.body 8 | 9 | const response = await db.newGroup({name, containers}) 10 | res.json(response) 11 | } 12 | 13 | exports.fetch = async (req, res) => { 14 | const response = await db.getGroups() 15 | res.json(response) 16 | } 17 | 18 | exports.delete = async (req, res) => { 19 | const { id } = req.body 20 | await db.deleteGroup(id) 21 | res.json([]) 22 | } -------------------------------------------------------------------------------- /backend/controllers/ImageController.js: -------------------------------------------------------------------------------- 1 | const { safeTerminal } = require("../utilities/terminal"); 2 | const { lightImageDetail } = require("../utilities/lightImageDetail"); 3 | 4 | exports.fetch = async (req, res) => { 5 | const images = await safeTerminal.formattedImages(); 6 | const imagesArray = images 7 | .split("\n") 8 | .filter((image) => image !== "") 9 | .map((image) => JSON.parse(image)); 10 | res.json(imagesArray); 11 | }; 12 | 13 | exports.command = async (req, res, next) => { 14 | const imageID = req.query.image; 15 | const command = req.query.command; 16 | try { 17 | const cmdData = await safeTerminal.singleImage(command, imageID); 18 | res.json(cmdData.replace("\n", "")); 19 | } catch (error) { 20 | next(error); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cors = require("cors"); 3 | const path = require("path"); 4 | const app = express(); 5 | const port = 3230; 6 | 7 | app.use(express.json()); 8 | app.use(cors()); 9 | app.use(express.static(path.join(__dirname, "web"))); 10 | 11 | // Boot database. 12 | const db = require("./utilities/db"); 13 | db.boot(); 14 | 15 | const { DefaultController } = require("./controllers/DefaultController"); 16 | const { 17 | GenericCommandController, 18 | } = require("./controllers/GenericCommandController"); 19 | const ContainerController = require("./controllers/ContainerController"); 20 | const ImageController = require("./controllers/ImageController"); 21 | const GroupController = require("./controllers/GroupController"); 22 | const CleanUpController = require("./controllers/CleanUpController"); 23 | 24 | app.get("/", DefaultController); 25 | app.get("/api/generic", GenericCommandController); 26 | 27 | app.get("/api/container/fetch", ContainerController.fetch); 28 | app.get("/api/container/fetchById", ContainerController.fetchById); 29 | app.get("/api/container/command", ContainerController.command); 30 | app.get("/api/container/logs", ContainerController.logs); 31 | app.get("/api/container/stats", ContainerController.stats); 32 | 33 | app.get("/api/image/fetch", ImageController.fetch); 34 | app.get("/api/image/command", ImageController.command); 35 | app.get("/api/cleanup/command", CleanUpController.command); 36 | 37 | app.post("/api/groups", GroupController.create); 38 | app.get("/api/groups", GroupController.fetch); 39 | app.delete("/api/groups", GroupController.delete); 40 | 41 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 42 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-web-gui", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "repository": "git@github.com:rakibtg/docker-web-gui.git", 6 | "author": "rakibtg ", 7 | "license": "MIT", 8 | "dependencies": { 9 | "cors": "^2.8.5", 10 | "express": "^4.17.1", 11 | "file-exists": "^5.0.1", 12 | "knex": "^0.19.4", 13 | "sqlite3": "^4.1.0", 14 | "write": "^2.0.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/utilities/db.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const resolve = require('path').resolve 3 | const knexLibrary = require('knex') 4 | const write = require('write') 5 | const fileExists = require('file-exists') 6 | 7 | exports.dbSource = resolve(__dirname+'/../data.db') 8 | 9 | exports.knex = knexLibrary({ 10 | client: 'sqlite3', 11 | connection: { 12 | filename: this.dbSource 13 | }, 14 | useNullAsDefault: true 15 | }) 16 | 17 | exports.boot = () => { 18 | fileExists(this.dbSource) 19 | .then( async exists => { 20 | if( !exists ) { 21 | // Create the file. 22 | await write(this.dbSource, '') 23 | // Create tables. 24 | await this.knex.schema.createTable('groups', function (table) { 25 | table.increments() 26 | table.string('name') 27 | table.text('containers_id') 28 | table.timestamps() 29 | }) 30 | } 31 | }) 32 | } 33 | 34 | exports.newGroup = ({name, containers}) => { 35 | return this.knex('groups').insert({ 36 | name, 37 | containers_id: JSON.stringify(containers), 38 | created_at: this.knex.fn.now(), 39 | updated_at: this.knex.fn.now(), 40 | }) 41 | } 42 | 43 | exports.deleteGroup = id => { 44 | return this.knex('groups').where('id', id).del() 45 | } 46 | 47 | exports.getGroups = () => { 48 | return this.knex('groups') 49 | .select() 50 | .orderBy('id', 'desc') 51 | } 52 | 53 | exports.getGroupById = id => this.knex('groups') 54 | .select() 55 | .where('id', id) -------------------------------------------------------------------------------- /backend/utilities/lightContainerDetail.js: -------------------------------------------------------------------------------- 1 | exports.lightContainerDetail = (id, inspectedData) => ({ 2 | Id: inspectedData.Id, 3 | shortId: id, 4 | Created: inspectedData.Created, 5 | State: inspectedData.State, 6 | Name: inspectedData.Name.replace('/', '') 7 | }) -------------------------------------------------------------------------------- /backend/utilities/lightImageDetail.js: -------------------------------------------------------------------------------- 1 | exports.lightImageDetail = (image, inspectedData) => ({ 2 | Id: image.ID, 3 | Repository: inspectedData.RepoTags[0], 4 | Created: inspectedData.Created, 5 | Size: image.Size, 6 | VirtualSize: image.VirtualSize 7 | }) -------------------------------------------------------------------------------- /backend/utilities/terminal.js: -------------------------------------------------------------------------------- 1 | const child_process = require("child_process"); 2 | 3 | const isValidId = (id) => /^[0-9a-zA-Z]+$/.test(id.trim()); 4 | const isValidString = (id) => /^[a-zA-Z]+$/.test(id.trim()); 5 | 6 | const Terminal = (command) => 7 | new Promise((resolve, reject) => { 8 | child_process.exec( 9 | command, 10 | { maxBuffer: 1500 * 1024 }, 11 | function (error, stdout, stderr) { 12 | if (!!error) reject(error); 13 | else resolve(stdout || stderr); 14 | } 15 | ); 16 | }); 17 | 18 | exports.safeTerminal = { 19 | installModules: async (backendPath) => { 20 | await Terminal(`cd ${backendPath} && npm install`); 21 | }, 22 | serve: async (backendPath) => { 23 | await Terminal(`cd ${backendPath} && node index.js`); 24 | }, 25 | allContainers: () => Terminal(`docker ps -q -a`), 26 | inspectContainer: async (id) => { 27 | if (isValidId(id)) { 28 | return Terminal(`docker container inspect ${id}`); 29 | } else { 30 | throw new Error("The container id is invalid"); 31 | } 32 | }, 33 | generic: async (task, id) => { 34 | if (!isValidString(task)) { 35 | throw new Error("The task command is invalid."); 36 | } 37 | if (!isValidId(id)) { 38 | throw new Error("The container id is invalid"); 39 | } 40 | return Terminal(`docker container ${task} ${id}`); 41 | }, 42 | logs: async (id) => { 43 | if (!isValidId(id)) { 44 | throw new Error("The container id is invalid"); 45 | } 46 | return Terminal(`docker container logs ${id} --tail 1500`); 47 | }, 48 | stats: () => 49 | Terminal( 50 | `docker container stats --no-stream --format '{"id": "{{.ID}}", "cpu_percentage": "{{.CPUPerc}}", "memory_usage": "{{.MemUsage}}", "network_io": "{{.NetIO}}"}'` 51 | ), 52 | prune: (pruneType) => { 53 | if (!isValidString(pruneType)) { 54 | throw new Error("The entity type is not valid"); 55 | } 56 | return Terminal(`docker ${pruneType} prune -f`); 57 | }, 58 | containerLs: () => Terminal(`docker container ls --format '{{json .}}'`), 59 | formattedImages: () => 60 | Terminal( 61 | `docker images --format '{"ID": "{{.ID}}", "Tag": "{{.Tag}}", "CreatedSince": "{{.CreatedSince}}", "Size": "{{.Size}}", "VirtualSize": "{{.VirtualSize}}", "Repository": "{{.Repository}}"}'` 62 | ), 63 | singleImage: (task, id) => { 64 | if (!isValidString(task)) { 65 | throw new Error("The task command is invalid."); 66 | } 67 | if (!isValidId(id)) { 68 | throw new Error("The image id is invalid"); 69 | } 70 | if (task == "run") { 71 | return Terminal(`docker ${task} ${id}`); 72 | } else { 73 | return Terminal(`docker image ${task} ${id}`); 74 | } 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /backend/web/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.ab6a2c25.chunk.css", 4 | "main.js": "/static/js/main.5147f23d.chunk.js", 5 | "main.js.map": "/static/js/main.5147f23d.chunk.js.map", 6 | "runtime~main.js": "/static/js/runtime~main.e38743dc.js", 7 | "runtime~main.js.map": "/static/js/runtime~main.e38743dc.js.map", 8 | "static/js/2.f3d6def3.chunk.js": "/static/js/2.f3d6def3.chunk.js", 9 | "static/js/2.f3d6def3.chunk.js.map": "/static/js/2.f3d6def3.chunk.js.map", 10 | "index.html": "/index.html", 11 | "precache-manifest.adb95cf9ceba078f9a2aef310d78b377.js": "/precache-manifest.adb95cf9ceba078f9a2aef310d78b377.js", 12 | "service-worker.js": "/service-worker.js", 13 | "static/css/main.ab6a2c25.chunk.css.map": "/static/css/main.ab6a2c25.chunk.css.map" 14 | } 15 | } -------------------------------------------------------------------------------- /backend/web/docker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtg/docker-web-gui/a8098abf8e0333a3cd081263cc4d9534743dab10/backend/web/docker-icon.png -------------------------------------------------------------------------------- /backend/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtg/docker-web-gui/a8098abf8e0333a3cd081263cc4d9534743dab10/backend/web/favicon.ico -------------------------------------------------------------------------------- /backend/web/index.html: -------------------------------------------------------------------------------- 1 | Docker Management Interface
-------------------------------------------------------------------------------- /backend/web/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtg/docker-web-gui/a8098abf8e0333a3cd081263cc4d9534743dab10/backend/web/logo192.png -------------------------------------------------------------------------------- /backend/web/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtg/docker-web-gui/a8098abf8e0333a3cd081263cc4d9534743dab10/backend/web/logo512.png -------------------------------------------------------------------------------- /backend/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/web/precache-manifest.adb95cf9ceba078f9a2aef310d78b377.js: -------------------------------------------------------------------------------- 1 | self.__precacheManifest = (self.__precacheManifest || []).concat([ 2 | { 3 | "revision": "9f5dd4b3b8d5e2a5e8a098dcc010693c", 4 | "url": "/index.html" 5 | }, 6 | { 7 | "revision": "6032ca2c0c37d3a1e3fd", 8 | "url": "/static/css/main.ab6a2c25.chunk.css" 9 | }, 10 | { 11 | "revision": "666d35ac2d9669737dde", 12 | "url": "/static/js/2.f3d6def3.chunk.js" 13 | }, 14 | { 15 | "revision": "6032ca2c0c37d3a1e3fd", 16 | "url": "/static/js/main.5147f23d.chunk.js" 17 | }, 18 | { 19 | "revision": "83e7f8b54e61b1cbddf8", 20 | "url": "/static/js/runtime~main.e38743dc.js" 21 | } 22 | ]); -------------------------------------------------------------------------------- /backend/web/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /backend/web/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your Workbox-powered service worker! 3 | * 4 | * You'll need to register this file in your web app and you should 5 | * disable HTTP caching for this file too. 6 | * See https://goo.gl/nhQhGp 7 | * 8 | * The rest of the code is auto-generated. Please don't update this file 9 | * directly; instead, make changes to your Workbox build configuration 10 | * and re-run your build process. 11 | * See https://goo.gl/2aRDsh 12 | */ 13 | 14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); 15 | 16 | importScripts( 17 | "/precache-manifest.adb95cf9ceba078f9a2aef310d78b377.js" 18 | ); 19 | 20 | self.addEventListener('message', (event) => { 21 | if (event.data && event.data.type === 'SKIP_WAITING') { 22 | self.skipWaiting(); 23 | } 24 | }); 25 | 26 | workbox.core.clientsClaim(); 27 | 28 | /** 29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to 30 | * requests for URLs in the manifest. 31 | * See https://goo.gl/S9QRab 32 | */ 33 | self.__precacheManifest = [].concat(self.__precacheManifest || []); 34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); 35 | 36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), { 37 | 38 | blacklist: [/^\/_/,/\/[^\/?]+\.[^\/]+$/], 39 | }); 40 | -------------------------------------------------------------------------------- /backend/web/static/css/main.ab6a2c25.chunk.css: -------------------------------------------------------------------------------- 1 | body{margin:0!important;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif!important;-webkit-font-smoothing:antialiased!important;-moz-osx-font-smoothing:grayscale!important}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace!important}div.subnavaware-view{height:calc(100vh - 148px)!important;overflow-x:hidden!important;overflow-y:scroll!important}.element-card{width:100%!important;max-width:755px!important;border-color:transparent!important}.card-active{border-width:1!important;border-color:#d6e0ef!important;background-color:#f9fbff!important;display:inline!important}.groupOptToggler{cursor:pointer!important}.groupOptToggler:hover{color:#0f71c1!important}.modal{display:block!important;position:fixed!important;z-index:10!important;padding-top:100px!important;left:0!important;top:0!important;width:100%!important;height:100%!important;overflow:auto!important;background-color:#000!important;background-color:rgba(0,0,0,.4)!important}.modal-pane{background-color:#fefefe!important;margin:auto!important;padding:10px!important;border:1px solid #888!important;width:80%!important;height:100px!important;width:500px}.modal-close{color:#aaa!important;float:right!important;font-size:28px!important;font-weight:700!important}.modal-close:focus,.modal-close:hover{color:#000!important;text-decoration:none!important;cursor:pointer!important} 2 | /*# sourceMappingURL=main.ab6a2c25.chunk.css.map */ -------------------------------------------------------------------------------- /backend/web/static/css/main.ab6a2c25.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["index.css","card.css","GroupCard.css","modal.css"],"names":[],"mappings":"AAAA,KACE,kBAAoB,CACpB,6IAEuB,CACvB,4CAA8C,CAC9C,2CACF,CAEA,KACE,iFAEF,CAEA,qBACE,oCAAsC,CACtC,2BAA6B,CAC7B,2BAEF,CCnBA,cACE,oBAAsB,CACtB,yBAA2B,CAC3B,kCACF,CAEA,aACE,wBAA0B,CAC1B,8BAAgC,CAChC,kCAAoC,CACpC,wBACF,CCXA,iBACE,wBACF,CAEA,uBACE,uBACF,CCNA,OACG,uBAAyB,CACzB,wBAA0B,CAC1B,oBAAsB,CACtB,2BAA6B,CAC7B,gBAAkB,CAClB,eAAiB,CACjB,oBAAsB,CACtB,qBAAuB,CACvB,uBAAyB,CACzB,+BAAuC,CACvC,yCACF,CAEA,YACE,kCAAoC,CACpC,qBAAuB,CACvB,sBAAwB,CACxB,+BAAiC,CACjC,mBAAqB,CACrB,sBAAwB,CACxB,WACF,CACA,aACE,oBAAyB,CACzB,qBAAuB,CACvB,wBAA0B,CAC1B,yBACF,CAEA,sCAEE,oBAAsB,CACtB,8BAAgC,CAChC,wBACF","file":"main.ab6a2c25.chunk.css","sourcesContent":["body {\n margin: 0 !important;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\",\n \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n sans-serif !important;\n -webkit-font-smoothing: antialiased !important;\n -moz-osx-font-smoothing: grayscale !important;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, \"Courier New\",\n monospace !important;\n}\n\ndiv.subnavaware-view {\n height: calc(100vh - 148px) !important;\n overflow-x: hidden !important;\n overflow-y: scroll !important;\n /* background: #e2e2e2; */\n}",".element-card {\n width: 100% !important;\n max-width: 755px !important;\n border-color: transparent !important;\n}\n\n.card-active {\n border-width: 1 !important;\n border-color: #d6e0ef !important;\n background-color: #f9fbff !important;\n display: inline !important;\n}\n",".groupOptToggler {\n cursor: pointer !important;\n}\n\n.groupOptToggler:hover {\n color: #0f71c1 !important;\n}",".modal {\n display: block !important; \n position: fixed !important; \n z-index: 10 !important; \n padding-top: 100px !important; \n left: 0 !important;\n top: 0 !important;\n width: 100% !important; \n height: 100% !important; \n overflow: auto !important; \n background-color: rgb(0,0,0) !important; \n background-color: rgba(0,0,0,0.4) !important; \n }\n \n .modal-pane {\n background-color: #fefefe !important;\n margin: auto !important;\n padding: 10px !important;\n border: 1px solid #888 !important;\n width: 80% !important;\n height: 100px !important;\n width: 500px\n }\n .modal-close {\n color: #aaaaaa !important;\n float: right !important;\n font-size: 28px !important;\n font-weight: bold !important;\n }\n \n .modal-close:hover,\n .modal-close:focus {\n color: #000 !important;\n text-decoration: none !important;\n cursor: pointer !important;\n }"]} -------------------------------------------------------------------------------- /backend/web/static/js/main.5147f23d.chunk.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonpclient=window.webpackJsonpclient||[]).push([[0],{196:function(e,t,n){},197:function(e,t,n){},198:function(e,t,n){"use strict";n.r(t);var a=n(0),r=n.n(a),o=n(23),i=n.n(o),c=(n(89),n(13)),l=n(35),s=n(33),u=n(2),d=n(4),g=n(6),p=n(5),m=n(7),f=n(8),h=n(30),b=n(55),y=n(37),O=n(47),j=function(e){function t(){var e,n;Object(d.a)(this,t);for(var a=arguments.length,r=new Array(a),o=0;o2&&void 0!==arguments[2]?arguments[2]:{},a={method:e,data:n,url:D+t,timeout:5e4};return C()(a)},k=n(218);function x(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,a)}return n}function P(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:"active";return function(t){t(G({loading:e,pageError:!1,segment:e,activeIndex:0,containerListLoading:!0})),L("get","container/fetch?status=".concat(e),{}).then(function(e){t(G({loading:!1,containers:e.data,pageError:!1,containerListLoading:!1}))}).catch(function(e){t(G({loading:!1,pageError:!0,containerListLoading:!1}))})}},M=function(e,t,n){return function(a){a(R({containerId:e.shortId,data:{stateToggling:!0}})),L("get","container/command?container=".concat(e.shortId,"&command=").concat(t)).then(function(r){var o=P({},e.State,{},{Running:"start"===t});a(R({containerId:e.shortId,data:{State:o,stateToggling:!1}})),n||k.a.success("Container ".concat(e.Name," has been ").concat("start"===t?"started":"stopped","."),{duration:5})}).catch(function(n){a(R({containerId:e.shortId,data:{stateToggling:!1}})),k.a.warning("There is problem ".concat("start"===t?"starting":"stopping"," container ").concat(e.Name),{duration:5})})}},A=function(e,t){return function(n){n(R({containerId:e.shortId,data:{stateToggling:!0,State:P({},e.State,{},{Running:!1})}})),L("get","container/command?container=".concat(e.shortId,"&command=").concat(t)).then(function(t){n(R({containerId:e.shortId,data:{State:P({},e.State,{},{Running:!0}),stateToggling:!1}})),k.a.success("Container ".concat(e.Name," has been restarted."),{duration:5})}).catch(function(t){n(R({containerId:e.shortId,data:{State:P({},e.State,{},{Running:!1}),stateToggling:!1}})),k.a.warning("There is problem restarting container ".concat(e.Name),{duration:5})})}},z=function(e,t){return function(n,a){L("get","container/command?container=".concat(e.shortId,"&command=").concat(t)).then(function(t){n({type:"DELETE_CONTAINER",payload:{containerId:e.shortId,showModal:!a().container.showModal,selectedContainer:{}}}),k.a.success("Container ".concat(e.Name," is no more!!!."),{duration:5})})}},_=function(e){return function(t){t(T({container:e,isShowingSideSheet:!1})),L("get","container/logs?container=".concat(e.shortId)).then(function(n){t(T({container:e,isShowingSideSheet:!0,logData:n.data}))})}},B=function(){return function(e,t){e(T({isShowingSideSheet:!t().container.isShowingSideSheet}))}},F=function(e){return function(t,n){t({type:"TOGGLE_MODAL",payload:{showModal:!n().container.showModal,selectedContainer:e||{}}})}},U=n(41),W=n(77),V={stats:{containerStats:[],isLive:!1},groups:{groups:[],selectedItems:[],showGroupsPage:!1,showNewGroupForm:!1,activeIndex:0,newGroupName:"",createFormLoading:!1,groupListLoading:!0,groupsRunning:[],groupsSwitchDisabled:[]},container:{containers:[],loading:!1,containerListLoading:!0,pageError:!1,segment:"active",activeIndex:0,isShowingSideSheet:!1,logData:{},showModal:!1,selectedContainer:{}},image:{images:[],loading:!1,pageError:!1,activeIndex:0,isShowingSideSheet:!1,logData:{},showModal:!1,selectedImage:{}},cleanup:{segmentOptions:[{label:"Prune Images",value:"image",message:"This action will allow you to clean up unused images. It cleans up dangling images. A dangling image is one that is not tagged and is not referenced by any container."},{label:"Prune Containers",value:"container",message:"When you stop a container, it is not automatically removed unless you started it with the --rm flag. A stopped container\u2019s writable layers still take up disk space."},{label:"Prune Volumes",value:"volume",message:"Volumes can be used by one or more containers, and take up space on the Docker host. Volumes are never removed automatically, because to do so could destroy data."},{label:"Prune System",value:"system",message:"Remove all unused containers, networks, images (both dangling and unreferenced), and optionally, volumes."}],selectedSegment:{label:"Prune Images",value:"image",message:"This action will allow you to clean up unused images. It cleans up dangling images. A dangling image is one that is not tagged and is not referenced by any container."},responseData:{},isShowingSideSheet:!1,apiCallStarted:!1}};function H(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,a)}return n}function J(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:null,t=arguments.length>1?arguments[1]:void 0;switch(t.type){case"GENERIC_STATS":return J({},e,{},t.payload);default:return e}},groups:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments.length>1?arguments[1]:void 0;switch(t.type){case"GENERIC_GROUPS":return Y({},e,{},t.payload);default:return e}},container:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments.length>1?arguments[1]:void 0;switch(t.type){case"GENERIC_CONTAINER":return q({},e,{},t.payload);case"UPDATE_CONTAINER":return q({},e,{},{containers:e.containers.map(function(e){return e.shortId==t.payload.containerId?q({},e,{},t.payload.data):e})});case"DELETE_CONTAINER":return q({},e,{},{containers:e.containers.filter(function(e){return e.shortId!==t.payload.containerId}),showModal:t.payload.showModal,selectedContainer:t.payload.selectedContainer});case"UPDATE_LOG":return q({},e,{},{logData:t.payload.logData&&t.payload.container?{container:t.payload.container,data:t.payload.logData}:{},isShowingSideSheet:t.payload.isShowingSideSheet});case"TOGGLE_MODAL":return q({},e,{},{showModal:t.payload.showModal,selectedContainer:t.payload.selectedContainer});default:return e}},image:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments.length>1?arguments[1]:void 0;switch(t.type){case"GENERIC_IMAGE":return Q({},e,{},t.payload);case"RUN_IMAGE":return Q({},e,{},{images:e.images.map(function(e){return e.ID==t.payload.imageId?Q({},e,{},t.payload.data):e})});case"DELETE_IMAGE":return Q({},e,{},{images:e.images.filter(function(e){return t.payload.imageId?e.ID!==t.payload.imageId:e}),showModal:t.payload.showModal,selectedImage:t.payload.selectedImage});case"TOGGLE_IMAGE_MODAL":return Q({},e,{},{showModal:t.payload.showModal,selectedImage:t.payload.selectedImage});default:return e}},cleanup:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments.length>1?arguments[1]:void 0;switch(t.type){case"SELECTED_SEGMENT":return ee({},e,{},{selectedSegment:e.segmentOptions.find(function(e){return e.value===t.payload.segmentValue})});case"EXECUTE_PRUNE":return ee({},e,{},{isShowingSideSheet:t.payload.isShowingSideSheet,responseData:t.payload.responseData?{data:t.payload.responseData}:{},apiCallStarted:t.payload.apiCallStarted});default:return e}}}),ne=[W.a],ae=Object(S.d)(function(e,t){return te(e,t)},V,S.a.apply(void 0,ne)),re=function(e){return{type:"GENERIC_GROUPS",payload:e}},oe=function(e){return function(t){var n=ae.getState().groups.selectedItems;if(n.includes(e)){var a=n.filter(function(t){return t!=e});t(re({selectedItems:a}))}else{var r=[].concat(Object(U.a)(n),[e]);t(re({selectedItems:r}))}}},ie=function(e,t,n){return function(a){var r=ae.getState().groups[e];if(n){var o=r.filter(function(e){return e!=t});a(re(Object(u.a)({},e,o)))}else{var i=[].concat(Object(U.a)(r),[t]);a(re(Object(u.a)({},e,i)))}}},ce=function(e){return function(t){t(re({createFormLoading:!0})),L("post","groups",{name:e.newGroupName,containers:e.selectedItems}).then(function(e){setTimeout(function(){t(re({newGroupName:"",selectedItems:[],showGroupsPage:!0,showNewGroupForm:!1,createFormLoading:!1}))},1200)})}},le=function(){return function(e){e(re({groupListLoading:!0})),L("get","groups",{}).then(function(t){e(re({groups:t.data,groupListLoading:!1}))})}},se=function(e){return function(t){L("delete","groups",{id:e}).then(function(e){t(le())})}},ue=n(212),de=function(e){function t(){var e,n;Object(d.a)(this,t);for(var a=arguments.length,r=new Array(a),o=0;o"!=t.Repository?t.Repository:"No Repository",": ").concat(""!=t.Tag?t.Tag:"No Tag"))),r.a.createElement(he.a,{backgroundColor:"#e7e9ef",fontWeight:"bold",borderRadius:16,paddingLeft:10,fontSize:11,paddingRight:10,marginLeft:10,marginTop:3},t.ID),r.a.createElement(he.a,{backgroundColor:"#d4eee3",fontWeight:"bold",borderRadius:16,paddingLeft:10,fontSize:11,paddingRight:10,marginLeft:10,marginTop:3},t.Size),r.a.createElement(he.a,{backgroundColor:"#deebf7",fontWeight:"bold",borderRadius:16,paddingLeft:10,fontSize:11,paddingRight:10,marginLeft:10,marginTop:3},t.CreatedSince)),l&&r.a.createElement(y.a,{display:"flex",marginTop:12},r.a.createElement(O.a,{marginRight:5,height:22,iconBefore:"application",onClick:function(){c(t)},isLoading:t.stateToggling},"Run"),r.a.createElement(O.a,{marginRight:5,height:22,iconBefore:"trash",onClick:function(){i(t)}},"Delete")))}}]),t}(r.a.PureComponent),gt=Object(c.b)(function(e){return{activeIndex:e.image.activeIndex}},function(e){return Object(S.b)({genericImage:$e,toggleImageDeleteModal:ot,runImageToContainer:at},e)})(dt),pt=function(e){return r.a.createElement(y.a,{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"center",marginTop:20},r.a.createElement(fe.a,null,e.spinner?"Loading images. Please wait....":"No image on this machine"),e.spinner&&r.a.createElement(w.a,{marginX:"auto",marginY:120}))},mt=function(e){function t(){return Object(d.a)(this,t),Object(p.a)(this,Object(m.a)(t).apply(this,arguments))}return Object(f.a)(t,e),Object(g.a)(t,[{key:"componentDidMount",value:function(){this.props.getImages()}},{key:"render",value:function(){var e=this.props,t=e.images,n=e.showModal,a=e.selectedImage,o=e.toggleImageDeleteModal;return e.loading?r.a.createElement(pt,{spinner:!0}):0==t.length?r.a.createElement(pt,null):r.a.createElement(y.a,{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"center",marginTop:20},n&&r.a.createElement(lt,{image:a,toggleModal:o}),t.map(function(e,t){return r.a.createElement(gt,{key:t,index:t,image:e})}))}}]),t}(r.a.PureComponent),ft=Object(c.b)(function(e){return{images:e.image.images,showModal:e.image.showModal,selectedImage:e.image.selectedImage,loading:e.image.loading}},function(e){return Object(S.b)({getImages:nt,toggleImageDeleteModal:ot},e)})(mt),ht=function(e){function t(){return Object(d.a)(this,t),Object(p.a)(this,Object(m.a)(t).apply(this,arguments))}return Object(f.a)(t,e),Object(g.a)(t,[{key:"render",value:function(){return r.a.createElement(r.a.Fragment,null,r.a.createElement(ft,null))}}]),t}(r.a.PureComponent),bt=function(e){return{type:"EXECUTE_PRUNE",payload:e}},yt=function(e){return function(t,n){t({type:"SELECTED_SEGMENT",payload:{segmentValue:e}})}},Ot=function(e){return function(t,n){t(bt({apiCallStarted:!0,responseData:{},isShowingSideSheet:!1})),L("get","cleanup/command?type=".concat(e)).then(function(n){t(bt({isShowingSideSheet:!!n.data,responseData:n.data,apiCallStarted:!1})),n.data||k.a.success("".concat(e," prune success"),{duration:5})}).catch(function(e){t(bt({responseData:{},isShowingSideSheet:!1,apiCallStarted:!1}))})}},jt=function(){return function(e,t){e(bt({isShowingSideSheet:!t().cleanup.isShowingSideSheet,apiCallStarted:t().cleanup.apiCallStarted}))}},Et=function(e){function t(){return Object(d.a)(this,t),Object(p.a)(this,Object(m.a)(t).apply(this,arguments))}return Object(f.a)(t,e),Object(g.a)(t,[{key:"render",value:function(){var e=this.props,t=e.segmentOptions,n=e.selectedSegment,a=e.setSelectedCleanUpSegment;return r.a.createElement(y.a,{backgroundColor:"#f1f1f1",display:"flex",justifyContent:"center",padding:10},r.a.createElement(v.a,{width:600,height:26,options:t,value:n.value,onChange:function(e){a(e)}}))}}]),t}(r.a.PureComponent),vt=Object(c.b)(function(e){return{segmentOptions:e.cleanup.segmentOptions,selectedSegment:e.cleanup.selectedSegment}},function(e){return Object(S.b)({setSelectedCleanUpSegment:yt},e)})(Et),wt=function(e){function t(){return Object(d.a)(this,t),Object(p.a)(this,Object(m.a)(t).apply(this,arguments))}return Object(f.a)(t,e),Object(g.a)(t,[{key:"render",value:function(){var e=this.props,t=e.selectedSegment,n=e.pruneCommand,a=e.apiCallStarted;return r.a.createElement(y.a,{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"center",marginTop:20},r.a.createElement(y.a,{display:"flex",flexDirection:"column",flexGrow:1,padding:12,borderRadius:6,border:"default",className:"element-card card-active"},r.a.createElement(y.a,{display:"flex"},r.a.createElement(y.a,{display:"flex"},r.a.createElement(fe.a,null,"Prune Docker ".concat(t.value,"s")))),r.a.createElement(y.a,{display:"flex",marginTop:12},r.a.createElement(Ze.a,null,t.message," ")),r.a.createElement(y.a,{display:"flex",marginTop:12},r.a.createElement(O.a,{marginRight:5,height:22,iconBefore:"trash",intent:"danger",isLoading:a,onClick:function(){n(t.value)}},"Proceed to Prune ".concat(t.value)))))}}]),t}(r.a.PureComponent),St=Object(c.b)(function(e){return{selectedSegment:e.cleanup.selectedSegment,apiCallStarted:e.cleanup.apiCallStarted}},function(e){return Object(S.b)({pruneCommand:Ot},e)})(wt),It=function(e){function t(){return Object(d.a)(this,t),Object(p.a)(this,Object(m.a)(t).apply(this,arguments))}return Object(f.a)(t,e),Object(g.a)(t,[{key:"render",value:function(){var e=this.props,t=e.resetLogSideSheet,n=e.isShowingSideSheet,a=e.logData;return r.a.createElement(r.a.Fragment,null,r.a.createElement(Ke,{resetLogSideSheet:t,isShowingSideSheet:n,logData:a}),r.a.createElement(vt,null),r.a.createElement(St,null))}}]),t}(r.a.PureComponent),Ct=Object(c.b)(function(e){return{isShowingSideSheet:e.cleanup.isShowingSideSheet,logData:e.cleanup.responseData}},function(e){return Object(S.b)({resetLogSideSheet:jt},e)})(It),Dt=function(){return r.a.createElement(r.a.Fragment,null,r.a.createElement(l.a,null,r.a.createElement(E,null),r.a.createElement(s.c,null,r.a.createElement(s.a,{path:"/",exact:!0,component:ut}),r.a.createElement(s.a,{path:"/containers",exact:!0,component:ut}),r.a.createElement(s.a,{path:"/images",component:ht}),r.a.createElement(s.a,{path:"/cleanup",component:Ct}))))};i.a.render(r.a.createElement(c.a,{store:ae},r.a.createElement(Dt,null)),document.getElementById("root"))},43:function(e,t,n){},84:function(e,t,n){e.exports=n(198)},89:function(e,t,n){}},[[84,1,2]]]); 2 | //# sourceMappingURL=main.5147f23d.chunk.js.map -------------------------------------------------------------------------------- /backend/web/static/js/runtime~main.e38743dc.js: -------------------------------------------------------------------------------- 1 | !function(e){function r(r){for(var n,l,i=r[0],f=r[1],a=r[2],p=0,s=[];p 11 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 12 | 13 | The page will reload if you make edits.
14 | You will also see any lint errors in the console. 15 | 16 | ### `npm test` 17 | 18 | Launches the test runner in the interactive watch mode.
19 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 20 | 21 | ### `npm run build` 22 | 23 | Builds the app for production to the `build` folder.
24 | It correctly bundles React in production mode and optimizes the build for the best performance. 25 | 26 | The build is minified and the filenames include the hashes.
27 | Your app is ready to be deployed! 28 | 29 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 30 | 31 | ### `npm run eject` -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.21.1", 7 | "evergreen-ui": "^4.18.1", 8 | "javascript-time-ago": "^2.0.1", 9 | "react": "^16.9.0", 10 | "react-dom": "^16.9.0", 11 | "react-redux": "^7.1.0", 12 | "react-router-dom": "^5.0.1", 13 | "react-scripts": "3.1.1", 14 | "redux": "^4.0.4", 15 | "redux-logger": "^3.0.6", 16 | "redux-thunk": "^2.3.0" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/public/docker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtg/docker-web-gui/a8098abf8e0333a3cd081263cc4d9534743dab10/client/public/docker-icon.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtg/docker-web-gui/a8098abf8e0333a3cd081263cc4d9534743dab10/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Docker Management Interface 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtg/docker-web-gui/a8098abf8e0333a3cd081263cc4d9534743dab10/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakibtg/docker-web-gui/a8098abf8e0333a3cd081263cc4d9534743dab10/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /client/src/components/Loader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Button, Heading, Badge, Spinner } from 'evergreen-ui' 3 | import '../components/container/style/card.css' 4 | const Loader = (props) => { 5 | return ( 6 | 12 | {props.spinner ? `Loading images. Please wait....` : `No image on this machine`} 13 | {props.spinner && } 14 | 15 | ) 16 | }; 17 | 18 | export default Loader -------------------------------------------------------------------------------- /client/src/components/LogSideSheet.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Pre, SideSheet, Heading, Paragraph } from 'evergreen-ui' 3 | 4 | class LogSideSheet extends React.PureComponent { 5 | 6 | render() { 7 | const { isShowingSideSheet, logData, resetLogSideSheet } = this.props 8 | return 18 | 19 | {logData && logData.container && 20 | 21 | Container logs 22 | 23 | {`Container Name: ${logData.container.Name}`} 24 | 25 | 26 | } 27 | { !logData.container && 28 | 29 | Prune response 30 | 31 | } 32 | 33 | 34 | 35 | {logData &&
{logData.data}
} 36 |
37 |
38 |
39 | } 40 | } 41 | 42 | export default LogSideSheet -------------------------------------------------------------------------------- /client/src/components/NavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, withRouter } from 'react-router-dom' 3 | import { Pane, Icon, Text, Button } from 'evergreen-ui' 4 | 5 | 6 | class NavBar extends React.PureComponent { 7 | 8 | state = { 9 | active: 'container' 10 | } 11 | 12 | componentDidMount () { 13 | let path = this.props.location.pathname 14 | if( path === '/' ) path = 'containers' 15 | this.setState({ 16 | active: path.replace('/', '') 17 | }) 18 | } 19 | 20 | navButton (name, icon) { 21 | return 22 | 23 | { name } 24 | 25 | } 26 | 27 | render () { 28 | const { active } = this.state 29 | return 30 | 44 | 60 | 75 | 76 | } 77 | } 78 | 79 | export default withRouter(NavBar) -------------------------------------------------------------------------------- /client/src/components/SecondaryNavBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Spinner, SegmentedControl, Button } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | 7 | import { getContainers } from '../store/actions/container.action' 8 | import { genericGroups } from '../store/actions/groups.action' 9 | 10 | import NewGroupForm from './groups/NewGroupForm' 11 | 12 | class SecondaryNavBar extends React.PureComponent { 13 | 14 | containerFilters () { 15 | const { loading, segment, getContainers } = this.props 16 | return : 'All', value: 'all' }, 21 | { label: loading === 'active' ? : 'Active', value: 'active' }, 22 | { label: loading === 'stopped' ? : 'Stopped', value: 'stopped' } 23 | ]} 24 | value={segment} 25 | onChange={value => { 26 | getContainers(value) 27 | }} 28 | /> 29 | } 30 | 31 | newGroupButton () { 32 | const { showNewGroupForm, genericGroups, getContainers } = this.props 33 | return 49 | } 50 | 51 | groupsToggler () { 52 | const { genericGroups, showGroupsPage, showNewGroupForm } = this.props 53 | const isBack = showGroupsPage || showNewGroupForm 54 | return 68 | } 69 | 70 | renderBody () { 71 | const { showNewGroupForm, showGroupsPage } = this.props 72 | if(showNewGroupForm) { 73 | return 74 | } else if (showGroupsPage) { 75 | return this.newGroupButton() 76 | } else { 77 | return this.containerFilters() 78 | } 79 | } 80 | 81 | render() { 82 | return 87 | {this.groupsToggler()} 88 | {this.renderBody()} 89 | 90 | } 91 | } 92 | 93 | const mapStateToProps = state => { 94 | return { 95 | segment: state.container.segment, 96 | loading: state.container.loading, 97 | showGroupsPage: state.groups.showGroupsPage, 98 | showNewGroupForm: state.groups.showNewGroupForm, 99 | } 100 | } 101 | 102 | const mapDispatchToProps = dispatch => bindActionCreators( 103 | { 104 | getContainers, 105 | genericGroups, 106 | }, 107 | dispatch 108 | ) 109 | 110 | export default connect(mapStateToProps, mapDispatchToProps)( SecondaryNavBar ) -------------------------------------------------------------------------------- /client/src/components/cleanup/cleanUpInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Button, Heading, Paragraph } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import '../container/style/card.css' 7 | import { pruneCommand } from '../../store/actions/cleanUp.action' 8 | 9 | class CleanUpInfo extends React.PureComponent { 10 | 11 | render () { 12 | 13 | const { selectedSegment, pruneCommand, apiCallStarted } = this.props 14 | return 20 | 29 | 30 | 31 | {`Prune Docker ${selectedSegment.value}s`} 32 | 33 | 34 | 35 | {selectedSegment.message} 36 | 37 | 38 | 49 | 50 | 51 | 52 | } 53 | 54 | } 55 | 56 | const mapStateToProps = state => { 57 | return { 58 | selectedSegment: state.cleanup.selectedSegment, 59 | apiCallStarted: state.cleanup.apiCallStarted 60 | } 61 | } 62 | 63 | const mapDispatchToProps = dispatch => bindActionCreators( 64 | { 65 | pruneCommand 66 | }, 67 | dispatch 68 | ) 69 | 70 | export default connect(mapStateToProps, mapDispatchToProps)( CleanUpInfo ) -------------------------------------------------------------------------------- /client/src/components/cleanup/cleanupSubNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Spinner, SegmentedControl, Button } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { setSelectedCleanUpSegment } from '../../store/actions/cleanUp.action' 7 | 8 | 9 | class CleanUpNavBar extends React.PureComponent { 10 | 11 | render() { 12 | const {segmentOptions, selectedSegment, setSelectedCleanUpSegment} = this.props; 13 | 14 | return 19 | { 25 | setSelectedCleanUpSegment(value) 26 | }} 27 | /> 28 | 29 | } 30 | } 31 | 32 | const mapStateToProps = state => { 33 | return { 34 | segmentOptions: state.cleanup.segmentOptions, 35 | selectedSegment: state.cleanup.selectedSegment 36 | } 37 | } 38 | 39 | const mapDispatchToProps = dispatch => bindActionCreators( 40 | { 41 | setSelectedCleanUpSegment 42 | }, 43 | dispatch 44 | ) 45 | 46 | export default connect(mapStateToProps, mapDispatchToProps)( CleanUpNavBar ) -------------------------------------------------------------------------------- /client/src/components/container/card.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Button, Heading, Badge } from 'evergreen-ui' 3 | import './style/card.css' 4 | 5 | import { bindActionCreators } from 'redux' 6 | import { connect } from 'react-redux' 7 | import { genericContainer, getLog, toggleDeleteModal } from '../../store/actions/container.action' 8 | 9 | import ContainerSwitch from './switch' 10 | import ContainerSelector from './selector' 11 | import ContainerRestart from './restartButton' 12 | import ContainerStat from './stat' 13 | import CreatedAt from '../createdAt' 14 | 15 | class ContainerCard extends React.PureComponent { 16 | 17 | actionButtons (active) { 18 | const { container, showNewGroupForm, toggleDeleteModal, getLog } = this.props 19 | if(!showNewGroupForm) { 20 | if(active) { 21 | return 22 | 23 | 29 | 38 | 39 | } else { 40 | return null 41 | } 42 | } 43 | } 44 | 45 | renderStats (container) { 46 | const { showNewGroupForm } = this.props 47 | if(!showNewGroupForm) { 48 | if(container.State.Running) { 49 | return 50 | } 51 | } 52 | } 53 | 54 | renderInfo (container) { 55 | const { showStatsInNewLine } = this.props 56 | const marginLeft = !!showStatsInNewLine ? 35:0 57 | const marginTop = !!showStatsInNewLine ? 5:0 58 | return 59 | { this.renderStats(container) } 60 | 61 | } 62 | 63 | render () { 64 | const { container, activeIndex, genericContainer, index, showNewGroupForm, noHoverStyle, showStatsInNewLine } = this.props 65 | const active = activeIndex == index 66 | let cardName = 'element-card' 67 | if(!noHoverStyle) { 68 | if(active) { 69 | cardName += ' card-active' 70 | } 71 | } 72 | const showColumn = !!showStatsInNewLine ? 'column':'row' 73 | return genericContainer({ 82 | activeIndex: index 83 | })}> 84 | 85 | 86 | { 87 | showNewGroupForm 88 | ? 89 | : 90 | } 91 | {container.Name} 92 | {container.shortId} 101 | 102 | 103 | 104 | { this.renderInfo(container) } 105 | 106 | { this.actionButtons(active) } 107 | 108 | } 109 | } 110 | 111 | const mapStateToProps = state => { 112 | return { 113 | activeIndex: state.container.activeIndex, 114 | showNewGroupForm: state.groups.showNewGroupForm, 115 | } 116 | } 117 | 118 | const mapDispatchToProps = dispatch => bindActionCreators( 119 | { 120 | genericContainer, 121 | getLog, 122 | toggleDeleteModal 123 | }, 124 | dispatch 125 | ) 126 | 127 | export default connect(mapStateToProps, mapDispatchToProps)( ContainerCard ) -------------------------------------------------------------------------------- /client/src/components/container/deleteModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './style/modal.css' 4 | import { Pane, Button, Heading, Icon } from 'evergreen-ui' 5 | import { bindActionCreators } from 'redux' 6 | import { connect } from 'react-redux' 7 | import { deleteContainer } from '../../store/actions/container.action' 8 | import { deleteImage } from '../../store/actions/image.action' 9 | 10 | const modalRoot = document.getElementById('modal-root') 11 | 12 | 13 | class Modal extends Component { 14 | constructor(props) { 15 | super(props); 16 | this.el = document.createElement('div'); 17 | this.handleDelete = this.handleDelete.bind(this) 18 | } 19 | 20 | componentDidMount() { 21 | modalRoot.appendChild(this.el); 22 | } 23 | 24 | componentWillUnmount() { 25 | modalRoot.removeChild(this.el); 26 | } 27 | 28 | handleDelete(){ 29 | if(this.props.container){ 30 | this.props.deleteContainer(this.props.container, 'rm') 31 | } else { 32 | console.log('else') 33 | this.props.deleteImage(this.props.image, 'rm') 34 | } 35 | } 36 | 37 | render() { 38 | const { container, image } = this.props 39 | return ReactDOM.createPortal( 40 |
41 | 49 | 50 | 51 | {`Are you sure to delete this ${container ? 'container' : 'image'}?`} 52 | 53 | 54 | 56 | 57 | 58 | 59 | {container ? container.Name : image.ID} 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
, 69 | this.el, 70 | ); 71 | } 72 | } 73 | 74 | const mapDispatchToProps = dispatch => bindActionCreators( 75 | { 76 | deleteContainer, 77 | deleteImage 78 | }, 79 | dispatch 80 | ) 81 | 82 | export default connect(null, mapDispatchToProps)(Modal) -------------------------------------------------------------------------------- /client/src/components/container/lists.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { getContainers, toggleDeleteModal, resetLogSideSheet } from '../../store/actions/container.action' 7 | 8 | import ContainerCard from './card' 9 | 10 | class ContainersList extends React.PureComponent { 11 | 12 | componentDidMount () { 13 | this.props.getContainers(this.props.segment) 14 | } 15 | 16 | render () { 17 | const { containers } = this.props 18 | return 24 | 25 | { 26 | containers.map((container, index) => 27 | 32 | ) 33 | } 34 | 35 | } 36 | 37 | } 38 | 39 | const mapStateToProps = state => { 40 | return { 41 | segment: state.container.segment, 42 | containers: state.container.containers, 43 | } 44 | } 45 | 46 | const mapDispatchToProps = dispatch => bindActionCreators( 47 | { 48 | getContainers 49 | }, 50 | dispatch 51 | ) 52 | 53 | export default connect(mapStateToProps, mapDispatchToProps)( ContainersList ) -------------------------------------------------------------------------------- /client/src/components/container/restartButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { restartContainer } from '../../store/actions/container.action' 7 | 8 | class ContainerRestartButton extends React.PureComponent { 9 | render () { 10 | const { container, restartContainer } = this.props 11 | const disabled = !!container.stateToggling 12 | return 23 | } 24 | } 25 | 26 | const mapDispatchToProps = dispatch => bindActionCreators( 27 | { 28 | restartContainer 29 | }, 30 | dispatch 31 | ) 32 | 33 | export default connect(null, mapDispatchToProps)( ContainerRestartButton ) -------------------------------------------------------------------------------- /client/src/components/container/selector.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Checkbox } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { groupItemSelector } from '../../store/actions/groups.action' 7 | 8 | class ContainerSelector extends React.PureComponent { 9 | 10 | render () { 11 | const { groupItemSelector, selectedItems } = this.props 12 | const shortId = this.props.container.shortId 13 | const checked = selectedItems.includes(shortId) 14 | return { 20 | groupItemSelector(shortId) 21 | }} 22 | /> 23 | } 24 | } 25 | 26 | const mapStateToProps = state => { 27 | return { 28 | selectedItems: state.groups.selectedItems, 29 | } 30 | } 31 | 32 | const mapDispatchToProps = dispatch => bindActionCreators( 33 | { 34 | groupItemSelector 35 | }, 36 | dispatch 37 | ) 38 | 39 | export default connect(mapStateToProps, mapDispatchToProps)( ContainerSelector ) -------------------------------------------------------------------------------- /client/src/components/container/stat.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Badge } from 'evergreen-ui' 3 | import { connect } from 'react-redux' 4 | 5 | 6 | class ContainerStat extends React.PureComponent { 7 | 8 | getMemoryUsage(memory) { 9 | [memory] = memory.split('/') 10 | let memoryFormated = memory.replace(/[a-zA-Z]/g, '').trim() 11 | return Number(memoryFormated).toFixed(1) + 'mb' 12 | } 13 | 14 | renderBadges () { 15 | const { stats, containerID } = this.props 16 | const data = stats 17 | .find(n => n.id === containerID) 18 | return data 19 | ? <> 20 | 21 | cpu {data.cpu_percentage} 22 | 23 | 24 | ram {this.getMemoryUsage(data.memory_usage)} 25 | 26 | 27 | net {this.getMemoryUsage(data.network_io)} 28 | 29 | 30 | : null 31 | } 32 | 33 | render () { 34 | return this.renderBadges() 35 | } 36 | } 37 | 38 | const mapStateToProps = state => { 39 | return { 40 | stats: state.stats.containerStats 41 | } 42 | } 43 | 44 | export default connect(mapStateToProps, null)( ContainerStat ) -------------------------------------------------------------------------------- /client/src/components/container/style/card.css: -------------------------------------------------------------------------------- 1 | .element-card { 2 | width: 100% !important; 3 | max-width: 755px !important; 4 | border-color: transparent !important; 5 | } 6 | 7 | .card-active { 8 | border-width: 1 !important; 9 | border-color: #d6e0ef !important; 10 | background-color: #f9fbff !important; 11 | display: inline !important; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/components/container/style/modal.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | display: block !important; 3 | position: fixed !important; 4 | z-index: 10 !important; 5 | padding-top: 100px !important; 6 | left: 0 !important; 7 | top: 0 !important; 8 | width: 100% !important; 9 | height: 100% !important; 10 | overflow: auto !important; 11 | background-color: rgb(0,0,0) !important; 12 | background-color: rgba(0,0,0,0.4) !important; 13 | } 14 | 15 | .modal-pane { 16 | background-color: #fefefe !important; 17 | margin: auto !important; 18 | padding: 10px !important; 19 | border: 1px solid #888 !important; 20 | width: 80% !important; 21 | height: 100px !important; 22 | width: 500px 23 | } 24 | .modal-close { 25 | color: #aaaaaa !important; 26 | float: right !important; 27 | font-size: 28px !important; 28 | font-weight: bold !important; 29 | } 30 | 31 | .modal-close:hover, 32 | .modal-close:focus { 33 | color: #000 !important; 34 | text-decoration: none !important; 35 | cursor: pointer !important; 36 | } -------------------------------------------------------------------------------- /client/src/components/container/switch.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Switch } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { toggleContainer } from '../../store/actions/container.action' 7 | 8 | class ContainerSwitch extends React.PureComponent { 9 | render () { 10 | const { container, toggleContainer } = this.props 11 | const command = container.State.Running 12 | ? 'stop' 13 | : 'start' 14 | const disabled = !!container.stateToggling 15 | return { 22 | toggleContainer(container, command) 23 | }} 24 | /> 25 | } 26 | } 27 | 28 | const mapDispatchToProps = dispatch => bindActionCreators( 29 | { 30 | toggleContainer 31 | }, 32 | dispatch 33 | ) 34 | 35 | export default connect(null, mapDispatchToProps)( ContainerSwitch ) -------------------------------------------------------------------------------- /client/src/components/createdAt.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Badge } from 'evergreen-ui' 3 | 4 | import TimeAgo from 'javascript-time-ago' 5 | import en from 'javascript-time-ago/locale/en' 6 | TimeAgo.locale(en) 7 | const timeAgo = new TimeAgo('en-US') 8 | 9 | const CreatedAt = props => { 10 | const twitterStyleTime = timeAgo.format(new Date(props.time), 'twitter') 11 | return 12 | {twitterStyleTime.trim() === '' ? 'now' : twitterStyleTime} 13 | 14 | } 15 | 16 | export default CreatedAt -------------------------------------------------------------------------------- /client/src/components/groups/GroupCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Button, Heading, Badge } from 'evergreen-ui' 3 | import './style/GroupCard.css' 4 | 5 | import { bindActionCreators } from 'redux' 6 | import { connect } from 'react-redux' 7 | 8 | import { genericGroups } from '../../store/actions/groups.action' 9 | 10 | import ContainerCard from '../container/card' 11 | import GroupSwitch from '../groups/GroupSwitch' 12 | import GroupDeleteButton from './GroupDeleteButton' 13 | 14 | class GroupCard extends React.PureComponent { 15 | 16 | filterContainers () { 17 | const { group, containers } = this.props 18 | const groupContainers = JSON.parse(group.containers_id) 19 | const filteredContainers = containers.filter(container => groupContainers.includes(container.shortId)) 20 | return filteredContainers 21 | } 22 | 23 | render () { 24 | const { group, genericGroups, index, activeIndex } = this.props 25 | const containers = this.filterContainers() 26 | if(containers.length <= 0) return null 27 | const active = activeIndex == index 28 | return 38 | 39 | 40 | 43 | genericGroups({ 48 | activeIndex: index 49 | })} 50 | size={600}>{group.name} 51 | {containers.length} 53 | 54 | 55 | 56 | 57 | 58 | 59 | { 60 | active && 61 | All Containers 62 | { 63 | containers.map((container, index) => { 64 | return 71 | }) 72 | } 73 | 74 | } 75 | 76 | 77 | } 78 | 79 | } 80 | 81 | const mapStateToProps = state => { 82 | return { 83 | containers: state.container.containers, 84 | activeIndex: state.groups.activeIndex, 85 | } 86 | } 87 | 88 | const mapDispatchToProps = dispatch => bindActionCreators( 89 | { 90 | genericGroups, 91 | }, 92 | dispatch 93 | ) 94 | 95 | export default connect(mapStateToProps, mapDispatchToProps)( GroupCard ) -------------------------------------------------------------------------------- /client/src/components/groups/GroupDeleteButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Dialog, Heading, Badge } from 'evergreen-ui' 3 | import ReactDOM from 'react-dom' 4 | 5 | import { bindActionCreators } from 'redux' 6 | import { connect } from 'react-redux' 7 | import { deleteGroup } from '../../store/actions/groups.action' 8 | 9 | class GroupDeleteButton extends React.PureComponent { 10 | 11 | state = { 12 | deleteModal: false, 13 | } 14 | 15 | renderDeleteModal () { 16 | const { groupName, groupId, deleteGroup } = this.props 17 | if(this.state.deleteModal) { 18 | return ReactDOM.createPortal( 19 | { 25 | deleteGroup(groupId) 26 | this.setState({ 27 | deleteModal: false, 28 | }) 29 | }} 30 | onCloseComplete={() => this.setState({ deleteModal: false })} 31 | confirmLabel="Confirm" 32 | > 33 | 34 | Are you sure you want to delete {groupName} container group? 35 | 36 | , 37 | document.body 38 | ) 39 | } else { 40 | return null 41 | } 42 | } 43 | 44 | render () { 45 | return <> 46 | 56 | {this.renderDeleteModal()} 57 | 58 | } 59 | } 60 | 61 | const mapDispatchToProps = dispatch => bindActionCreators( 62 | { 63 | deleteGroup, 64 | }, 65 | dispatch 66 | ) 67 | 68 | export default connect(null, mapDispatchToProps)( GroupDeleteButton ) -------------------------------------------------------------------------------- /client/src/components/groups/GroupSwitch.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Switch } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | 7 | import { groupStatusUpdater, genericGroups } from '../../store/actions/groups.action' 8 | import { toggleContainer } from '../../store/actions/container.action' 9 | 10 | class GroupSwitch extends React.PureComponent { 11 | 12 | isRunning () { 13 | const { containers } = this.props 14 | const runningContainers = containers.filter(c => c.State.Running === true) 15 | return containers.length === runningContainers.length 16 | } 17 | 18 | isLoading () { 19 | const { containers } = this.props 20 | const loadingContainers = containers.filter(c => !!c.stateToggling) 21 | return loadingContainers === 0 22 | } 23 | 24 | toggleAllContainers () { 25 | const { containers, toggleContainer, genericGroups, groupIndex } = this.props 26 | const isRunning = this.isRunning() 27 | const command = isRunning 28 | ? 'stop' 29 | : 'start' 30 | genericGroups({ 31 | activeIndex: groupIndex 32 | }) 33 | containers.map(container => { 34 | toggleContainer(container, command, true) 35 | }) 36 | } 37 | 38 | render () { 39 | const runningStatus = this.isRunning() 40 | return { 47 | this.toggleAllContainers() 48 | }} 49 | /> 50 | } 51 | } 52 | 53 | const mapStateToProps = state => { 54 | return { 55 | activeIndex: state.groups.activeIndex, 56 | } 57 | } 58 | 59 | const mapDispatchToProps = dispatch => bindActionCreators( 60 | { 61 | genericGroups, 62 | toggleContainer, 63 | groupStatusUpdater, 64 | }, 65 | dispatch 66 | ) 67 | 68 | export default connect(mapStateToProps, mapDispatchToProps)( GroupSwitch ) -------------------------------------------------------------------------------- /client/src/components/groups/GroupsList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Heading } from 'evergreen-ui' 3 | 4 | import GroupCard from './GroupCard' 5 | 6 | import { bindActionCreators } from 'redux' 7 | import { connect } from 'react-redux' 8 | 9 | import { getContainers } from '../../store/actions/container.action' 10 | import { getGroups } from '../../store/actions/groups.action' 11 | 12 | class GroupsList extends React.PureComponent { 13 | 14 | componentDidMount () { 15 | const { getGroups, getContainers } = this.props 16 | getGroups() 17 | getContainers('all') 18 | } 19 | 20 | renderGroupsList () { 21 | const { groups, groupListLoading, containerListLoading } = this.props 22 | if( groupListLoading && containerListLoading ) { 23 | return Please wait 24 | } 25 | if( groups.length <= 0 ) { 26 | return No groups found, please create a new one. 27 | } 28 | return groups.map((group, index) => { 29 | return 34 | }) 35 | } 36 | 37 | render () { 38 | return 44 | {this.renderGroupsList()} 45 | 46 | } 47 | } 48 | 49 | const mapStateToProps = state => { 50 | return { 51 | groups: state.groups.groups, 52 | groupListLoading: state.groups.groupListLoading, 53 | containerListLoading: state.container.containerListLoading, 54 | } 55 | } 56 | 57 | const mapDispatchToProps = dispatch => bindActionCreators( 58 | { 59 | getGroups, 60 | getContainers, 61 | }, 62 | dispatch 63 | ) 64 | 65 | export default connect(mapStateToProps, mapDispatchToProps)( GroupsList ) -------------------------------------------------------------------------------- /client/src/components/groups/NewGroupForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Button, TextInput, IconButton } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | 7 | import { genericGroups, createGroup } from '../../store/actions/groups.action' 8 | 9 | class NewGroupForm extends React.PureComponent { 10 | 11 | state = { 12 | toggleLink: false, 13 | } 14 | 15 | handleSubmit () { 16 | const { createGroup, newGroupName, selectedItems } = this.props 17 | createGroup({newGroupName, selectedItems}) 18 | } 19 | 20 | render () { 21 | const { selectedItems, newGroupName, genericGroups, createFormLoading } = this.props 22 | return 26 | 27 | { 34 | genericGroups({ 35 | newGroupName: e.target.value 36 | }) 37 | }} 38 | value={newGroupName} 39 | /> 40 | { 41 | this.state.toggleLink && { 49 | genericGroups({ 50 | newGroupName: e.target.value 51 | }) 52 | }} 53 | value={newGroupName} 54 | /> 55 | } 56 | 57 | 58 | {/* { 63 | e.preventDefault() 64 | this.setState({ 65 | toggleLink: !this.state.toggleLink 66 | }) 67 | }} 68 | /> 69 | */} 70 | 84 | 85 | 86 | } 87 | } 88 | 89 | const mapStateToProps = state => { 90 | return { 91 | newGroupName: state.groups.newGroupName, 92 | selectedItems: state.groups.selectedItems, 93 | createFormLoading: state.groups.createFormLoading, 94 | } 95 | } 96 | 97 | const mapDispatchToProps = dispatch => bindActionCreators( 98 | { 99 | createGroup, 100 | genericGroups, 101 | }, 102 | dispatch 103 | ) 104 | 105 | export default connect(mapStateToProps, mapDispatchToProps)( NewGroupForm ) -------------------------------------------------------------------------------- /client/src/components/groups/style/GroupCard.css: -------------------------------------------------------------------------------- 1 | .groupOptToggler { 2 | cursor: pointer !important; 3 | } 4 | 5 | .groupOptToggler:hover { 6 | color: #0f71c1 !important; 7 | } -------------------------------------------------------------------------------- /client/src/components/image/imageCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane, Button, Heading, Badge, Spinner } from 'evergreen-ui' 3 | import '../../components/container/style/card.css' 4 | 5 | import { bindActionCreators } from 'redux' 6 | import { connect } from 'react-redux' 7 | import { genericImage, runImageToContainer, toggleImageDeleteModal } from '../../store/actions/image.action' 8 | 9 | 10 | class ImageCard extends React.PureComponent { 11 | 12 | render () { 13 | const { image, activeIndex, genericImage, index, toggleImageDeleteModal, runImageToContainer } = this.props 14 | const active = activeIndex == index 15 | return genericImage({ 24 | activeIndex: index 25 | })}> 26 | 27 | 28 | {`${image.Repository != ''? image.Repository : 'No Repository'}: ${ image.Tag != '' ? image.Tag : 'No Tag'}`} 29 | 30 | {image.ID} 31 | {image.Size} 32 | {image.CreatedSince} 33 | 34 | { active && 35 | 36 | 44 | 53 | 54 | } 55 | 56 | } 57 | } 58 | 59 | const mapStateToProps = state => { 60 | return { 61 | activeIndex: state.image.activeIndex, 62 | } 63 | } 64 | 65 | const mapDispatchToProps = dispatch => bindActionCreators( 66 | { 67 | genericImage, 68 | toggleImageDeleteModal, 69 | runImageToContainer 70 | }, 71 | dispatch 72 | ) 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)( ImageCard ) -------------------------------------------------------------------------------- /client/src/components/image/imageLists.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Pane } from 'evergreen-ui' 3 | 4 | import { bindActionCreators } from 'redux' 5 | import { connect } from 'react-redux' 6 | import { getImages, toggleImageDeleteModal } from '../../store/actions/image.action' 7 | 8 | import ImageCard from './imageCard' 9 | import Modal from '../container/deleteModal' 10 | import Loader from '../Loader' 11 | 12 | class ImageList extends React.PureComponent { 13 | 14 | componentDidMount () { 15 | this.props.getImages() 16 | } 17 | 18 | render () { 19 | const { images, showModal, selectedImage, toggleImageDeleteModal, loading } = this.props 20 | if(loading){ 21 | return 22 | } else if(images.length == 0){ 23 | return 24 | } 25 | return 31 | 32 | { showModal && } 33 | { 34 | images.map((image, index) => 35 | 40 | ) 41 | } 42 | 43 | } 44 | 45 | } 46 | 47 | const mapStateToProps = state => { 48 | return { 49 | images: state.image.images, 50 | showModal: state.image.showModal, 51 | selectedImage: state.image.selectedImage, 52 | loading: state.image.loading 53 | } 54 | } 55 | 56 | const mapDispatchToProps = dispatch => bindActionCreators( 57 | { 58 | getImages, 59 | toggleImageDeleteModal 60 | }, 61 | dispatch 62 | ) 63 | 64 | export default connect(mapStateToProps, mapDispatchToProps)( ImageList ) -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0 !important; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif !important; 6 | -webkit-font-smoothing: antialiased !important; 7 | -moz-osx-font-smoothing: grayscale !important; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace !important; 13 | } 14 | 15 | div.subnavaware-view { 16 | height: calc(100vh - 148px) !important; 17 | overflow-x: hidden !important; 18 | overflow-y: scroll !important; 19 | /* background: #e2e2e2; */ 20 | } -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | 5 | import { Provider } from 'react-redux' 6 | import Routes from './routes' 7 | import { store } from './store' 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ) -------------------------------------------------------------------------------- /client/src/pages/cleanup.page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { bindActionCreators } from 'redux' 4 | 5 | import CleanUpNavBar from '../components/cleanup/cleanupSubNav' 6 | import CleanUpInfo from '../components/cleanup/cleanUpInfo' 7 | import { resetLogSideSheet } from '../store/actions/cleanUp.action' 8 | import LogSideSheet from '../components/LogSideSheet' 9 | 10 | import { connect } from 'react-redux' 11 | 12 | class CleanUpPage extends React.PureComponent { 13 | render () { 14 | const { resetLogSideSheet, isShowingSideSheet, logData } = this.props 15 | return <> 16 | 17 | 18 | 19 | 20 | } 21 | 22 | } 23 | 24 | const mapStateToProps = state => { 25 | return { 26 | isShowingSideSheet: state.cleanup.isShowingSideSheet, 27 | logData: state.cleanup.responseData 28 | } 29 | } 30 | 31 | const mapDispatchToProps = dispatch => bindActionCreators( 32 | { 33 | resetLogSideSheet 34 | }, 35 | dispatch 36 | ) 37 | 38 | export default connect(mapStateToProps, mapDispatchToProps)( CleanUpPage ) -------------------------------------------------------------------------------- /client/src/pages/container.page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import SecondaryNavBar from '../components/SecondaryNavBar' 4 | import ContainerLists from '../components/container/lists' 5 | import GroupsList from '../components/groups/GroupsList' 6 | 7 | import {containerStatsProcess} from '../store/actions/stats.action' 8 | 9 | import {store} from '../store' 10 | 11 | import { bindActionCreators } from 'redux' 12 | import { connect } from 'react-redux' 13 | import { toggleDeleteModal, resetLogSideSheet } from '../store/actions/container.action' 14 | import LogSideSheet from '../components/LogSideSheet' 15 | import Modal from '../components/container/deleteModal' 16 | 17 | class ContainerPage extends React.PureComponent { 18 | 19 | componentDidMount () { 20 | store.dispatch(containerStatsProcess()) 21 | } 22 | 23 | render () { 24 | const { showGroupsPage,showModal, selectedContainer, toggleDeleteModal, 25 | resetLogSideSheet, isShowingSideSheet, logData } = this.props 26 | return <> 27 | 28 | 29 | { showModal && } 30 |
31 | { 32 | showGroupsPage 33 | ? 34 | : 35 | } 36 |
37 | 38 | } 39 | 40 | } 41 | 42 | const mapStateToProps = state => { 43 | return { 44 | showGroupsPage: state.groups.showGroupsPage, 45 | showModal: state.container.showModal, 46 | selectedContainer: state.container.selectedContainer, 47 | isShowingSideSheet: state.container.isShowingSideSheet, 48 | logData: state.container.logData 49 | } 50 | } 51 | 52 | const mapDispatchToProps = dispatch => bindActionCreators( 53 | { 54 | toggleDeleteModal, 55 | resetLogSideSheet 56 | }, 57 | dispatch 58 | ) 59 | 60 | export default connect(mapStateToProps, mapDispatchToProps)( ContainerPage ) -------------------------------------------------------------------------------- /client/src/pages/image.page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import SecondaryNavBar from '../components/SecondaryNavBar' 4 | import ImageLists from '../components/image/imageLists' 5 | 6 | class ImagePage extends React.PureComponent { 7 | 8 | render () { 9 | return <> 10 | 11 | 12 | } 13 | 14 | } 15 | 16 | export default ImagePage -------------------------------------------------------------------------------- /client/src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {BrowserRouter, Route, Switch} from 'react-router-dom' 3 | import Navbar from './components/NavBar' 4 | 5 | import ContainerPage from './pages/container.page' 6 | import ImagePage from './pages/image.page' 7 | import CleanupPage from './pages/cleanup.page' 8 | 9 | const Routes = () => { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | export default Routes -------------------------------------------------------------------------------- /client/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /client/src/store/actions/cleanUp.action.js: -------------------------------------------------------------------------------- 1 | import { request } from '../../utilities/request' 2 | import { toaster } from 'evergreen-ui' 3 | 4 | export const setCleanUpSegment = payload => ({ 5 | type: 'SELECTED_SEGMENT', 6 | payload 7 | }) 8 | 9 | export const executePrune = payload => ({ 10 | type: 'EXECUTE_PRUNE', 11 | payload 12 | }) 13 | 14 | 15 | export const setSelectedCleanUpSegment = (value) => (dispatch, getState)=>{ 16 | dispatch(setCleanUpSegment({ 17 | segmentValue: value 18 | })) 19 | } 20 | 21 | 22 | export const pruneCommand = (type) => (dispatch, getState)=>{ 23 | dispatch(executePrune({ 24 | apiCallStarted: true, 25 | responseData: {}, 26 | isShowingSideSheet:false, 27 | })) 28 | request('get', `cleanup/command?type=${type}`) 29 | .then(res => { 30 | dispatch(executePrune({ 31 | isShowingSideSheet: res.data ? true : false, 32 | responseData: res.data, 33 | apiCallStarted: false 34 | })) 35 | if(!res.data){ 36 | toaster.success(`${type} prune success`, {duration: 5}) 37 | } 38 | }) 39 | .catch(ex=>{ 40 | dispatch(executePrune({ 41 | responseData: {}, 42 | isShowingSideSheet:false, 43 | apiCallStarted:false 44 | })) 45 | }) 46 | } 47 | 48 | export const resetLogSideSheet = () => (dispatch, getState)=>{ 49 | dispatch(executePrune({ 50 | isShowingSideSheet: !getState().cleanup.isShowingSideSheet, 51 | apiCallStarted: getState().cleanup.apiCallStarted 52 | })) 53 | } -------------------------------------------------------------------------------- /client/src/store/actions/container.action.js: -------------------------------------------------------------------------------- 1 | import { request } from '../../utilities/request' 2 | import { toaster } from 'evergreen-ui' 3 | 4 | export const genericContainer = payload => ({ 5 | type: 'GENERIC_CONTAINER', 6 | payload 7 | }) 8 | 9 | export const updateContainer = payload => ({ 10 | type: 'UPDATE_CONTAINER', 11 | payload 12 | }) 13 | 14 | export const removeContainer = payload => ({ 15 | type: 'DELETE_CONTAINER', 16 | payload 17 | }) 18 | 19 | export const updateContainerLog = payload => ({ 20 | type: 'UPDATE_LOG', 21 | payload 22 | }) 23 | 24 | export const toggleModal = payload => ({ 25 | type: 'TOGGLE_MODAL', 26 | payload 27 | }) 28 | 29 | export const getContainers = (status = 'active') => { 30 | return dispatch => { 31 | dispatch(genericContainer({ 32 | loading: status, 33 | pageError: false, 34 | segment: status, 35 | activeIndex: 0, 36 | containerListLoading: true, 37 | })) 38 | request('get', `container/fetch?status=${status}`, {}) 39 | .then(response => { 40 | dispatch(genericContainer({ 41 | loading: false, 42 | containers: response.data, 43 | pageError: false, 44 | containerListLoading: false, 45 | })) 46 | }).catch(error => { 47 | dispatch(genericContainer({ 48 | loading: false, 49 | pageError: true, 50 | containerListLoading: false, 51 | })) 52 | }) 53 | } 54 | } 55 | 56 | export const toggleContainer = (container, status, hideToaster) => { 57 | return dispatch => { 58 | dispatch(updateContainer({ 59 | containerId: container.shortId, 60 | data: { stateToggling: true }, 61 | })) 62 | request('get', `container/command?container=${container.shortId}&command=${status}`) 63 | .then(res => { 64 | const State = { 65 | ...container.State, 66 | ...{ 67 | Running: status === 'start' 68 | ? true 69 | : false 70 | } 71 | } 72 | dispatch(updateContainer({ 73 | containerId: container.shortId, 74 | data: { 75 | State, 76 | stateToggling: false, 77 | }, 78 | })) 79 | if(! !!hideToaster) { 80 | toaster.success( 81 | `Container ${container.Name} has been ${status === 'start'? 'started' : 'stopped'}.`, 82 | { duration: 5 } 83 | ) 84 | } 85 | }) 86 | .catch( ex => { 87 | dispatch(updateContainer({ 88 | containerId: container.shortId, 89 | data: { stateToggling: false }, 90 | })) 91 | toaster.warning(`There is problem ${status === 'start'? 'starting' : 'stopping'} container ${container.Name}`,{duration: 5}) 92 | }) 93 | } 94 | } 95 | 96 | export const restartContainer = (container, status) => { 97 | return dispatch => { 98 | dispatch(updateContainer({ 99 | containerId: container.shortId, 100 | data: { 101 | stateToggling: true, 102 | State: { 103 | ...container.State, 104 | ...{ 105 | Running: false 106 | } 107 | } 108 | }, 109 | })) 110 | request('get', `container/command?container=${container.shortId}&command=${status}`) 111 | .then(res => { 112 | dispatch(updateContainer({ 113 | containerId: container.shortId, 114 | data: { 115 | State: { 116 | ...container.State, 117 | ...{ 118 | Running: true 119 | } 120 | }, 121 | stateToggling: false, 122 | }, 123 | })) 124 | toaster.success(`Container ${container.Name} has been restarted.`,{ duration: 5 }) 125 | }) 126 | .catch( ex => { 127 | dispatch(updateContainer({ 128 | containerId: container.shortId, 129 | data: { 130 | State: { 131 | ...container.State, 132 | ...{ 133 | Running: false 134 | } 135 | }, 136 | stateToggling: false, 137 | }, 138 | })) 139 | toaster.warning(`There is problem restarting container ${container.Name}`,{duration: 5}) 140 | }) 141 | } 142 | } 143 | 144 | export const deleteContainer = (container, command) => (dispatch, getState)=>{ 145 | request('get', `container/command?container=${container.shortId}&command=${command}`) 146 | .then(res => { 147 | dispatch(removeContainer({ 148 | containerId: container.shortId, 149 | showModal: !getState().container.showModal, 150 | selectedContainer: {} 151 | })) 152 | toaster.success( 153 | `Container ${container.Name} is no more!!!.`, 154 | { 155 | duration: 5 156 | } 157 | ) 158 | }) 159 | } 160 | 161 | export const getLog = (container) => { 162 | return dispatch => { 163 | dispatch(updateContainerLog({ 164 | container: container, 165 | isShowingSideSheet: false, 166 | })) 167 | request('get', `container/logs?container=${container.shortId}`) 168 | .then(response => { 169 | dispatch(updateContainerLog({ 170 | container: container, 171 | isShowingSideSheet: true, 172 | logData: response.data 173 | })) 174 | }) 175 | } 176 | } 177 | 178 | export const resetLogSideSheet = () => (dispatch, getState)=>{ 179 | dispatch(updateContainerLog({ 180 | isShowingSideSheet: !getState().container.isShowingSideSheet, 181 | })) 182 | } 183 | 184 | export const toggleDeleteModal = (container) => (dispatch, getState)=>{ 185 | dispatch(toggleModal({ 186 | showModal: !getState().container.showModal, 187 | selectedContainer: container ? container : {} 188 | })) 189 | } -------------------------------------------------------------------------------- /client/src/store/actions/groups.action.js: -------------------------------------------------------------------------------- 1 | import { store } from '../' 2 | import { request } from '../../utilities/request' 3 | 4 | export const genericGroups = payload => ({ 5 | type: 'GENERIC_GROUPS', 6 | payload 7 | }) 8 | 9 | export const groupItemSelector = itemID => { 10 | return dispatch => { 11 | const selectedItems = store.getState().groups.selectedItems 12 | if(selectedItems.includes(itemID)) { 13 | // Remove item. 14 | const modifiedListOfItems = selectedItems.filter(value => value != itemID) 15 | dispatch(genericGroups({ 16 | selectedItems: modifiedListOfItems, 17 | })) 18 | } else { 19 | // Add item. 20 | const items = [ 21 | ...selectedItems, 22 | itemID 23 | ] 24 | dispatch(genericGroups({ 25 | selectedItems: items 26 | })) 27 | } 28 | } 29 | } 30 | 31 | export const groupStatusUpdater = (groupSchemaProperty, groupIndex, add) => { 32 | return dispatch => { 33 | const items = store.getState().groups[groupSchemaProperty] 34 | if(add) { 35 | // Remove the group index. 36 | const newItems = items.filter(value => value != groupIndex) 37 | dispatch(genericGroups({ 38 | [groupSchemaProperty]: newItems 39 | })) 40 | } else { 41 | // Add the group index. 42 | const newItems = [ 43 | ...items, 44 | groupIndex 45 | ] 46 | dispatch(genericGroups({ 47 | [groupSchemaProperty]: newItems 48 | })) 49 | } 50 | } 51 | } 52 | 53 | export const createGroup = data => { 54 | return dispatch => { 55 | dispatch(genericGroups({ createFormLoading: true })) 56 | request('post', 'groups', {name: data.newGroupName, containers: data.selectedItems}) 57 | .then(res => { 58 | setTimeout(() => { 59 | dispatch(genericGroups({ 60 | newGroupName: '', 61 | selectedItems: [], 62 | showGroupsPage: true, 63 | showNewGroupForm: false, 64 | createFormLoading: false, 65 | })) 66 | }, 1200) 67 | }) 68 | } 69 | } 70 | 71 | export const getGroups = () => { 72 | return dispatch => { 73 | dispatch(genericGroups({ 74 | groupListLoading: true, 75 | })) 76 | request('get', 'groups', {}) 77 | .then(res => { 78 | dispatch(genericGroups({ 79 | groups: res.data, 80 | groupListLoading: false, 81 | })) 82 | }) 83 | } 84 | } 85 | 86 | export const deleteGroup = groupId => { 87 | return dispatch => { 88 | request('delete', 'groups', {id: groupId}) 89 | .then(res => { 90 | dispatch(getGroups()) 91 | }) 92 | } 93 | } -------------------------------------------------------------------------------- /client/src/store/actions/image.action.js: -------------------------------------------------------------------------------- 1 | import { request } from '../../utilities/request' 2 | import { toaster } from 'evergreen-ui' 3 | 4 | export const genericImage = payload => ({ 5 | type: 'GENERIC_IMAGE', 6 | payload 7 | }) 8 | 9 | export const runImage = payload => ({ 10 | type: 'RUN_IMAGE', 11 | payload 12 | }) 13 | 14 | export const removeImage = payload => ({ 15 | type: 'DELETE_IMAGE', 16 | payload 17 | }) 18 | 19 | export const toggleModal = payload => ({ 20 | type: 'TOGGLE_IMAGE_MODAL', 21 | payload 22 | }) 23 | 24 | export const getImages = () => { 25 | return dispatch => { 26 | dispatch(genericImage({ 27 | loading: true, 28 | pageError: false, 29 | activeIndex: 0, 30 | })) 31 | request('get', `image/fetch`) 32 | .then(response => { 33 | dispatch(genericImage({ 34 | loading: false, 35 | images: response.data, 36 | pageError: false, 37 | })) 38 | }).catch(error => { 39 | dispatch(genericImage({ 40 | loading: false, 41 | pageError: true, 42 | })) 43 | }) 44 | } 45 | } 46 | 47 | export const runImageToContainer = (image) => { 48 | return dispatch => { 49 | dispatch(runImage({ 50 | imageId: image.ID, 51 | data: { stateToggling: true }, 52 | })) 53 | request('get', `image/command?image=${image.ID}&command=${'run'}`) 54 | .then(res => { 55 | dispatch(runImage({ 56 | imageId: image.ID, 57 | data: { stateToggling: false }, 58 | })) 59 | toaster.success(`Image ${image.ID} has been started running.`,{ duration: 5 }) 60 | }) 61 | .catch( ex => { 62 | dispatch(runImage({ 63 | imageId: image.ID, 64 | data: { stateToggling: false }, 65 | })) 66 | toaster.warning(`There is problem running image ${image.ID}`,{duration: 5}) 67 | }) 68 | } 69 | } 70 | 71 | 72 | export const deleteImage = (image, command) => (dispatch, getState)=>{ 73 | request('get', `image/command?image=${image.ID}&command=${command}`) 74 | .then(res => { 75 | dispatch(removeImage({ 76 | imageId: image.ID, 77 | showModal: !getState().image.showModal, 78 | selectedImage: {} 79 | })) 80 | toaster.success( 81 | `Image ${image.ID} is no more!!!.`, 82 | { 83 | duration: 5 84 | } 85 | ) 86 | }) 87 | .catch(ex=>{ 88 | dispatch(removeImage({ 89 | showModal: !getState().image.showModal, 90 | selectedImage: {} 91 | })) 92 | toaster.danger( 93 | `Image ${image.ID} can not be deleted! May be used by some containers.`, 94 | { 95 | duration: 5 96 | }, 97 | ) 98 | }) 99 | } 100 | 101 | export const toggleImageDeleteModal = (image) => (dispatch, getState)=>{ 102 | dispatch(toggleModal({ 103 | showModal: !getState().image.showModal, 104 | selectedImage: image ? image : {} 105 | })) 106 | } -------------------------------------------------------------------------------- /client/src/store/actions/stats.action.js: -------------------------------------------------------------------------------- 1 | import { store } from '../' 2 | import { request } from '../../utilities/request' 3 | 4 | export const genericStats = payload => ({ 5 | type: 'GENERIC_STATS', 6 | payload 7 | }) 8 | 9 | export const getContainersStat = () => { 10 | return dispatch => { 11 | request('get', `container/stats`, {}) 12 | .then(response => { 13 | dispatch(genericStats({ containerStats: response.data })) 14 | }).catch(error => { 15 | console.log(error) 16 | }) 17 | } 18 | } 19 | 20 | export const containerStatsProcess = () => { 21 | if(!store.getState().stats.isLive) { 22 | return dispatch => { 23 | dispatch(getContainersStat()) 24 | dispatch(genericStats({ isLive: true })) 25 | setInterval(() => { 26 | dispatch(getContainersStat()) 27 | }, 4000) 28 | } 29 | } else { 30 | return dispatch => { 31 | dispatch(genericStats({ isLive: true })) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunk from 'redux-thunk' 3 | 4 | import schema from './schema' 5 | import rootReducer from './reducers' 6 | const middlewares = [thunk] 7 | 8 | if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { 9 | const { createLogger } = require('redux-logger') 10 | const logger = createLogger({ 11 | collapsed: true, 12 | }) 13 | middlewares.push(logger) 14 | } 15 | 16 | export const store = createStore( 17 | rootReducer, 18 | schema, // Initial data. 19 | applyMiddleware(...middlewares) 20 | ) -------------------------------------------------------------------------------- /client/src/store/reducers/cleanUp.reducer.js: -------------------------------------------------------------------------------- 1 | export default (state = null, action) => { 2 | 3 | switch (action.type) { 4 | 5 | case 'SELECTED_SEGMENT': 6 | return { 7 | ...state, 8 | ...{ 9 | selectedSegment: state.segmentOptions.find(c => { 10 | return c.value === action.payload.segmentValue 11 | }) 12 | } 13 | } 14 | 15 | case 'EXECUTE_PRUNE': 16 | return { 17 | ...state, 18 | ...{ 19 | isShowingSideSheet: action.payload.isShowingSideSheet, 20 | responseData: action.payload.responseData ? { 21 | data: action.payload.responseData 22 | } : {}, 23 | apiCallStarted: action.payload.apiCallStarted 24 | } 25 | } 26 | 27 | default: 28 | return state 29 | 30 | } 31 | } -------------------------------------------------------------------------------- /client/src/store/reducers/container.reducer.js: -------------------------------------------------------------------------------- 1 | export default (state = null, action) => { 2 | 3 | switch (action.type) { 4 | 5 | case 'GENERIC_CONTAINER': 6 | return { 7 | ...state, 8 | ...action.payload 9 | } 10 | 11 | case 'UPDATE_CONTAINER': 12 | return { 13 | ...state, 14 | ...{ 15 | containers: state.containers.map(c => { 16 | if(c.shortId == action.payload.containerId) { 17 | return { 18 | ...c, 19 | ...action.payload.data 20 | } 21 | } else { 22 | return c 23 | } 24 | }) 25 | } 26 | } 27 | 28 | case 'DELETE_CONTAINER': 29 | return { 30 | ...state, 31 | ...{ 32 | containers: state.containers.filter(c => { 33 | return c.shortId !== action.payload.containerId 34 | }, 35 | ), 36 | showModal: action.payload.showModal, 37 | selectedContainer: action.payload.selectedContainer 38 | } 39 | } 40 | 41 | case 'UPDATE_LOG': 42 | return { 43 | ...state, 44 | ...{ 45 | logData: action.payload.logData && action.payload.container ? { 46 | container: action.payload.container , 47 | data: action.payload.logData 48 | } : {}, 49 | isShowingSideSheet: action.payload.isShowingSideSheet 50 | } 51 | } 52 | 53 | case 'TOGGLE_MODAL': 54 | return { 55 | ...state, 56 | ...{ 57 | showModal: action.payload.showModal, 58 | selectedContainer: action.payload.selectedContainer 59 | } 60 | } 61 | 62 | default: 63 | return state 64 | 65 | } 66 | } -------------------------------------------------------------------------------- /client/src/store/reducers/groups.reducer.js: -------------------------------------------------------------------------------- 1 | export default (state = null, action) => { 2 | 3 | switch (action.type) { 4 | 5 | case 'GENERIC_GROUPS': 6 | return { 7 | ...state, 8 | ...action.payload 9 | } 10 | 11 | default: 12 | return state 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /client/src/store/reducers/image.reducer.js: -------------------------------------------------------------------------------- 1 | export default (state = null, action) => { 2 | 3 | switch (action.type) { 4 | 5 | case 'GENERIC_IMAGE': 6 | return { 7 | ...state, 8 | ...action.payload 9 | } 10 | 11 | case 'RUN_IMAGE': 12 | return { 13 | ...state, 14 | ...{ 15 | images: state.images.map(c => { 16 | if(c.ID == action.payload.imageId) { 17 | return { 18 | ...c, 19 | ...action.payload.data 20 | } 21 | } else { 22 | return c 23 | } 24 | }) 25 | } 26 | } 27 | 28 | case 'DELETE_IMAGE': 29 | return { 30 | ...state, 31 | ...{ 32 | images: state.images.filter(c => { 33 | if(action.payload.imageId) { 34 | return c.ID !== action.payload.imageId 35 | } else { 36 | return c 37 | } 38 | }), 39 | showModal: action.payload.showModal, 40 | selectedImage: action.payload.selectedImage 41 | } 42 | } 43 | 44 | case 'TOGGLE_IMAGE_MODAL': 45 | return { 46 | ...state, 47 | ...{ 48 | showModal: action.payload.showModal, 49 | selectedImage: action.payload.selectedImage 50 | } 51 | } 52 | 53 | default: 54 | return state 55 | 56 | } 57 | } -------------------------------------------------------------------------------- /client/src/store/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | 3 | import stats from './stats.reducer' 4 | import groups from './groups.reducer' 5 | import container from './container.reducer' 6 | import image from './image.reducer' 7 | import cleanup from './cleanUp.reducer' 8 | 9 | const appReducer = combineReducers({ 10 | stats, 11 | groups, 12 | container, 13 | image, 14 | cleanup 15 | }) 16 | 17 | export default (state, action) => { 18 | return appReducer(state, action) 19 | } -------------------------------------------------------------------------------- /client/src/store/reducers/stats.reducer.js: -------------------------------------------------------------------------------- 1 | export default (state = null, action) => { 2 | 3 | switch (action.type) { 4 | 5 | case 'GENERIC_STATS': 6 | return { 7 | ...state, 8 | ...action.payload 9 | } 10 | 11 | default: 12 | return state 13 | 14 | } 15 | } -------------------------------------------------------------------------------- /client/src/store/schema/cleanup.schema.js: -------------------------------------------------------------------------------- 1 | export default { 2 | segmentOptions: [ 3 | { label: 'Prune Images', value: 'image', message: 'This action will allow you to clean up unused images. It cleans up dangling images. A dangling image is one that is not tagged and is not referenced by any container.' }, 4 | { label: 'Prune Containers', value: 'container', message: 'When you stop a container, it is not automatically removed unless you started it with the --rm flag. A stopped container’s writable layers still take up disk space.' }, 5 | { label: 'Prune Volumes', value: 'volume', message: 'Volumes can be used by one or more containers, and take up space on the Docker host. Volumes are never removed automatically, because to do so could destroy data.' }, 6 | { label: 'Prune System', value: 'system', message: 'Remove all unused containers, networks, images (both dangling and unreferenced), and optionally, volumes.' } 7 | ], 8 | selectedSegment: { label: 'Prune Images', value: 'image', message: 'This action will allow you to clean up unused images. It cleans up dangling images. A dangling image is one that is not tagged and is not referenced by any container.' }, 9 | responseData: {}, 10 | isShowingSideSheet: false, 11 | apiCallStarted: false 12 | } -------------------------------------------------------------------------------- /client/src/store/schema/container.schema.js: -------------------------------------------------------------------------------- 1 | export default { 2 | containers: [], 3 | loading: false, 4 | containerListLoading: true, 5 | pageError: false, 6 | segment: 'active', 7 | activeIndex: 0, 8 | isShowingSideSheet: false, 9 | logData: {}, 10 | showModal: false, 11 | selectedContainer: {} 12 | } -------------------------------------------------------------------------------- /client/src/store/schema/groups.schema.js: -------------------------------------------------------------------------------- 1 | export default { 2 | groups: [], 3 | selectedItems: [], 4 | showGroupsPage: false, 5 | showNewGroupForm: false, 6 | activeIndex: 0, 7 | newGroupName: '', 8 | createFormLoading: false, 9 | groupListLoading: true, 10 | groupsRunning: [], 11 | groupsSwitchDisabled: [], 12 | } -------------------------------------------------------------------------------- /client/src/store/schema/image.schema.js: -------------------------------------------------------------------------------- 1 | export default { 2 | images: [], 3 | loading: false, 4 | pageError: false, 5 | activeIndex: 0, 6 | isShowingSideSheet: false, 7 | logData: {}, 8 | showModal: false, 9 | selectedImage: {} 10 | } -------------------------------------------------------------------------------- /client/src/store/schema/index.js: -------------------------------------------------------------------------------- 1 | import stats from './stats.schema' 2 | import groups from './groups.schema' 3 | import container from './container.schema' 4 | import image from './image.schema' 5 | import cleanup from './cleanup.schema' 6 | 7 | export default { 8 | stats, 9 | groups, 10 | container, 11 | image, 12 | cleanup 13 | } -------------------------------------------------------------------------------- /client/src/store/schema/stats.schema.js: -------------------------------------------------------------------------------- 1 | export default { 2 | containerStats: [], 3 | isLive: false, 4 | } -------------------------------------------------------------------------------- /client/src/utilities/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | export const restPath = '/api/' 3 | 4 | export const request = ( method, path, data = {} ) => { 5 | const options = { 6 | method, 7 | data, 8 | url: restPath + path, 9 | timeout: 50000, 10 | } 11 | return axios(options) 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | docker-web-gui: 5 | container_name: docker-web-gui 6 | build: 7 | context: . 8 | ports: 9 | - "3230:3230" 10 | volumes: 11 | - /var/run/docker.sock:/var/run/docker.sock 12 | restart: always # This ensures the container auto-starts and restarts on failure 13 | --------------------------------------------------------------------------------