├── .gitignore ├── .vscode └── settings.json ├── app ├── index.tsx └── components │ ├── dialogTrigger.tsx │ ├── containerList.tsx │ ├── containerListItem.tsx │ ├── modal.tsx │ ├── newContainerModal.tsx │ └── app.tsx ├── tsconfig.json ├── dockerapi.js ├── webpack.config.js ├── index.html ├── README.md ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import { AppComponent } from './components/app' 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('app') 8 | ) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es5", 8 | "jsx": "react" 9 | } 10 | } -------------------------------------------------------------------------------- /dockerapi.js: -------------------------------------------------------------------------------- 1 | let Docker = require("dockerode"); 2 | let isWindows = process.platform === "win32"; 3 | 4 | let options = {}; 5 | 6 | if (isWindows) { 7 | options = { 8 | host: '127.0.0.1', 9 | port: 2375 10 | } 11 | } else { 12 | options = { 13 | socketPath: '/var/run/docker.sock' 14 | } 15 | } 16 | 17 | module.exports = new Docker(options); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./app/index.tsx", 3 | output: { 4 | filename: "bundle.js", 5 | path: __dirname + "/public/js" 6 | }, 7 | 8 | devtool: "source-map", 9 | 10 | resolve: { 11 | extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"] 12 | }, 13 | 14 | module: { 15 | loaders: [ 16 | { test: /\.tsx?$/, loader: "ts-loader" } 17 | ] 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /app/components/dialogTrigger.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | export interface DialogTriggerProperties { 4 | id: string 5 | buttonText: string 6 | } 7 | 8 | export class DialogTrigger extends React.Component { 9 | render() { 10 | const href = `#${this.props.id}` 11 | 12 | return ( 13 | { this.props.buttonText } 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /app/components/containerList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Container, ContainerListItem } from './containerListItem' 3 | 4 | export class ContainerListProps { 5 | containers: Container[] 6 | title?: string 7 | } 8 | 9 | export class ContainerList extends React.Component { 10 | render() { 11 | return ( 12 | 13 | {this.props.title} 14 | { this.props.containers.length == 0 ? "No containers to show" : "" } 15 | 16 | { this.props.containers.map(c => ) } 17 | 18 | 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Docker Dashboard 8 | 9 | 10 | 11 | 12 | 13 | 14 | Docker Dashboard! 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Docker Dashboard Example 2 | 3 | This is the code repository for the "Create a Docker dashboard with Typescript, React and Socket.io" article on Auth0. 4 | 5 | ## Prerequisites 6 | 7 | **Node 6.x** 8 | 9 | **Webpack 2** 10 | 11 | ``` 12 | npm install -g webpack 13 | ``` 14 | 15 | **Typescript 2.x** 16 | 17 | ``` 18 | npm install -g typescript 19 | ``` 20 | 21 | Clone the project, then run: 22 | 23 | ``` 24 | npm install 25 | npm link typescript 26 | ``` 27 | 28 | The sample interacts with Docker, so having the [native Docker tools](https://www.docker.com/) for your OS is required for the sample to be of any use. If you're looking for an image to play around with for the sample, feel free to use `elkdanger/express-app`: 29 | 30 | `docker pull elkdanger/express-app` 31 | 32 | ## Running the sample 33 | 34 | ``` 35 | npm start 36 | ``` 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-dashboard", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack -p && node server.js", 8 | "start-dev": "./node_modules/.bin/concurrently \"nodemon server.js\" \"webpack --watch\"" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "classnames": "^2.2.5", 14 | "dockerode": "^2.3.1", 15 | "express": "^4.14.0", 16 | "lodash": "^4.17.4", 17 | "react": "^15.4.1", 18 | "react-dom": "^15.4.1", 19 | "socket.io": "^1.7.2" 20 | }, 21 | "devDependencies": { 22 | "@types/bootstrap": "^3.3.32", 23 | "@types/classnames": "0.0.32", 24 | "@types/jquery": "^2.0.39", 25 | "@types/lodash": "^4.14.45", 26 | "@types/react": "^0.14.55", 27 | "@types/react-dom": "^0.14.20", 28 | "@types/socket.io": "^1.4.27", 29 | "@types/socket.io-client": "^1.4.29", 30 | "concurrently": "^3.4.0", 31 | "nodemon": "^1.11.0", 32 | "source-map-loader": "^0.1.5", 33 | "ts-loader": "^1.3.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/components/containerListItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as classNames from 'classnames' 3 | import * as io from 'socket.io-client' 4 | 5 | const socket = io.connect() 6 | 7 | export interface Container { 8 | id: string 9 | name: string 10 | image: string 11 | state: string 12 | status: string 13 | } 14 | 15 | export class ContainerListItem extends React.Component { 16 | 17 | onActionButtonClick() { 18 | const evt = this.isRunning() ? 'container.stop' : 'container.start' 19 | socket.emit(evt, { id: this.props.id }) 20 | } 21 | 22 | isRunning() { 23 | return this.props.state === 'running' 24 | } 25 | 26 | render() { 27 | const panelClass = this.isRunning() ? 'success' : 'default' 28 | const classes = classNames('panel', `panel-${panelClass}`) 29 | const buttonText = this.isRunning() ? 'Stop' : 'Start' 30 | 31 | return ( 32 | 33 | 34 | { this.props.name } 35 | 36 | Status: {this.props.status} 37 | Image: {this.props.image} 38 | 39 | 40 | {buttonText} 41 | 42 | 43 | 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /app/components/modal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | interface ModalProperties { 4 | id: string 5 | title: string 6 | buttonText?: string 7 | onButtonClicked?: () => boolean|undefined 8 | } 9 | 10 | export default class Modal extends React.Component { 11 | 12 | modalElementId: string 13 | 14 | constructor(props: ModalProperties) { 15 | super(props) 16 | this.modalElementId = `#${this.props.id}` 17 | } 18 | 19 | onPrimaryButtonClick() { 20 | if (this.props.onButtonClicked) { 21 | if (this.props.onButtonClicked() !== false) { 22 | $(this.modalElementId).modal('hide') 23 | } 24 | } 25 | } 26 | 27 | render() { 28 | return ( 29 | 30 | 31 | 32 | 33 | × 34 | { this.props.title } 35 | 36 | 37 | { this.props.children } 38 | 39 | 40 | { this.props.buttonText || "Ok" } 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | let express = require('express') 2 | let path = require('path') 3 | let app = express() 4 | let server = require('http').Server(app) 5 | let io = require('socket.io')(server) 6 | let docker = require('./dockerapi') 7 | 8 | // Use the environment port if available, or default to 3000 9 | let port = process.env.PORT || 3000 10 | 11 | // Serve static files from /public 12 | app.use(express.static('public')) 13 | 14 | // Create an endpoint which just returns the index.html page 15 | app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'index.html'))) 16 | 17 | // Start the server 18 | server.listen(port, () => console.log(`Server started on port ${port}`)) 19 | 20 | function refreshContainers() { 21 | docker.listContainers({ all: true}, (err, containers) => { 22 | io.emit('containers.list', containers) 23 | }) 24 | } 25 | 26 | setInterval(refreshContainers, 2000) 27 | 28 | io.on('connection', socket => { 29 | 30 | socket.on('containers.list', () => { 31 | refreshContainers() 32 | }) 33 | 34 | socket.on('container.start', args => { 35 | const container = docker.getContainer(args.id) 36 | 37 | if (container) { 38 | container.start((err, data) => refreshContainers()) 39 | } 40 | }) 41 | 42 | socket.on('container.stop', args => { 43 | const container = docker.getContainer(args.id) 44 | 45 | if (container) { 46 | container.stop((err, data) => refreshContainers()) 47 | } 48 | }) 49 | 50 | socket.on('image.run', args => { 51 | docker.createContainer({ Image: args.name }, (err, container) => { 52 | if (!err) 53 | container.start((err, data) => { 54 | if (err) 55 | socket.emit('image.error', { message: err }) 56 | }) 57 | else 58 | socket.emit('image.error', { message: err }) 59 | }) 60 | }) 61 | 62 | }) -------------------------------------------------------------------------------- /app/components/newContainerModal.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Modal from './modal' 3 | import * as classNames from 'classnames' 4 | 5 | interface ModalProperties { 6 | id: string, 7 | onRunImage?: (name: string) => void 8 | } 9 | 10 | interface ModalState { 11 | imageName: string 12 | isValid: boolean 13 | } 14 | 15 | export class NewContainerDialog extends React.Component { 16 | 17 | constructor(props: ModalProperties) { 18 | super(props) 19 | 20 | this.state = { 21 | imageName: '', 22 | isValid: false 23 | } 24 | } 25 | 26 | onImageNameChange(e: any) { 27 | const name = e.target.value 28 | 29 | this.setState({ 30 | imageName: name, 31 | isValid: name.length > 0 32 | }) 33 | } 34 | 35 | runImage() { 36 | if (this.state.isValid && this.props.onRunImage) 37 | this.props.onRunImage(this.state.imageName) 38 | 39 | return this.state.isValid 40 | } 41 | 42 | render() { 43 | 44 | let inputClass = classNames({ 45 | "form-group": true, 46 | "has-error": !this.state.isValid 47 | }) 48 | 49 | return ( 50 | 51 | 52 | 53 | Image name 54 | 55 | 60 | 61 | 62 | 63 | 64 | ) 65 | } 66 | } -------------------------------------------------------------------------------- /app/components/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Container, ContainerListItem } from './containerListItem' 3 | import { ContainerList } from './containerList' 4 | import * as _ from 'lodash' 5 | import * as io from 'socket.io-client' 6 | import { NewContainerDialog } from './newContainerModal' 7 | import { DialogTrigger } from './dialogTrigger' 8 | 9 | let socket = io.connect() 10 | 11 | class AppState { 12 | containers?: Container[] 13 | stoppedContainers?: Container[] 14 | } 15 | 16 | export class AppComponent extends React.Component<{}, AppState> { 17 | 18 | constructor() { 19 | super() 20 | this.state = { 21 | containers: [], 22 | stoppedContainers: [] 23 | } 24 | 25 | socket.on('containers.list', (containers: any) => { 26 | 27 | const partitioned = _.partition(containers, (c: any) => c.State == "running") 28 | 29 | this.setState({ 30 | containers: partitioned[0].map(this.mapContainer), 31 | stoppedContainers: partitioned[1].map(this.mapContainer) 32 | }) 33 | }) 34 | 35 | socket.on('image.error', (args: any) => { 36 | alert(args.message.json.message) 37 | }) 38 | } 39 | 40 | mapContainer(container:any): Container { 41 | return { 42 | id: container.Id, 43 | name: _.chain(container.Names) 44 | .map((n: string) => n.substr(1)) 45 | .join(", ") 46 | .value(), 47 | state: container.State, 48 | status: `${container.State} (${container.Status})`, 49 | image: container.Image 50 | } 51 | } 52 | 53 | componentDidMount() { 54 | socket.emit('containers.list') 55 | } 56 | 57 | onRunImage(name: String) { 58 | socket.emit('image.run', { name: name }) 59 | } 60 | 61 | render() { 62 | return ( 63 | 64 | Docker Dashboard 65 | 66 | 67 | 68 | 69 | 70 | 71 | ) 72 | } 73 | } --------------------------------------------------------------------------------
{ this.props.containers.length == 0 ? "No containers to show" : "" }