├── .gitignore ├── Dockerfile ├── README.md ├── api ├── .gitignore ├── app.js ├── bin │ └── www ├── public │ └── stylesheets │ │ └── style.css └── routes │ └── api.js ├── config ├── default.json └── test.json ├── data └── cache.db ├── package.json ├── public ├── favicon.ico ├── images │ ├── cloud.png │ ├── docker.png │ ├── file.png │ └── traefik.png └── index.html ├── src ├── App.css ├── App.js ├── App.test.js ├── Conf.js ├── actions │ └── index.js ├── components │ ├── Search.js │ ├── ThreeJSFlow.js │ ├── Tree.js │ └── UrlInput.js ├── containers │ ├── AsyncApp.js │ └── Root.js ├── index.css ├── index.js ├── reducers │ └── index.js └── store │ └── configureStore.js └── test ├── data └── cache.db └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # data 13 | /api/cache.db 14 | /data 15 | 16 | # misc 17 | .DS_Store 18 | .env 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:7-alpine 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | # Bundle app source 8 | COPY . /usr/src/app 9 | 10 | # Install all dependencies, executes post-install script and remove deps 11 | RUN npm install && npm cache clean && npm run build-front && rm -r node_modules 12 | 13 | # Install app production only dependencies 14 | RUN npm install --production && npm cache clean && cp -rp ./node_modules /tmp/node_modules 15 | 16 | EXPOSE 3001 17 | 18 | CMD [ "npm", "start" ] 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-traefik 2 | 3 | Interactive diagram view for traefik using React and D3 4 | 5 | [![Codefresh build status]( https://g.codefresh.io/api/badges/build?repoOwner=guillaumejacquart&repoName=react-traefik&branch=latest&pipelineName=react-traefik&accountName=guillaumejacquart&type=cf-1)]( https://g.codefresh.io/repositories/guillaumejacquart/react-traefik/builds?filter=trigger:build;branch:latest;service:591aebd418391f000191df52~react-traefik) 6 | 7 | ![alt text](https://image.ibb.co/enAZi5/Sans_titre.png "Screenshot") 8 | 9 | # Setup 10 | The easiest way to install the package is using docker. The docker runs a web application that talks to the traefik api and exposes its own api for the dashboard to call. 11 | 12 | ## Using docker 13 | The host you run the docker container on must have access to the traefik frontend for it to work. 14 | ```sh 15 | docker run -d -p 3001:3001 --network= ghiltoniel/traefik-react 16 | ``` 17 | 18 | Then go to [http://localhost:3001](http://localhost:3001) to access the dashboard 19 | You must fill out the traefik API URL on the header bar to access the dashboard 20 | 21 | ## Traefik service discovery 22 | The container can also automatically discover the traefik API using the docker API to get Traefik IP address. For that, you must map the docker socket to the container volumes : 23 | 24 | ```sh 25 | docker run -d -p 3001:3001 --network= -v /var/run/docker.sock:/var/run/docker.sock ghiltoniel/traefik-react 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # misc 7 | .DS_Store 8 | .env 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | -------------------------------------------------------------------------------- /api/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | var logger = require('morgan'); 4 | var bodyParser = require('body-parser'); 5 | var cors = require('cors'); 6 | 7 | var api = require('./routes/api'); 8 | 9 | var app = express(); 10 | app.use(cors()); 11 | 12 | // uncomment after placing your favicon in /public 13 | //app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))); 14 | app.use(logger('dev')); 15 | app.use(bodyParser.json()); 16 | app.use(bodyParser.urlencoded({ extended: false })); 17 | app.use(express.static(path.join(__dirname, '../build'))); 18 | 19 | app.use('/api', api); 20 | 21 | // catch 404 and forward to error handler 22 | app.use(function(req, res, next) { 23 | var err = new Error('Not Found'); 24 | err.status = 404; 25 | next(err); 26 | }); 27 | 28 | // error handler 29 | app.use(function(err, req, res, next) { 30 | // set locals, only providing error in development 31 | res.locals.message = err.message; 32 | res.locals.error = req.app.get('env') === 'development' ? err : {}; 33 | 34 | // render the error page 35 | res.status(err.status || 500); 36 | res.json(err); 37 | }); 38 | 39 | module.exports = app; 40 | -------------------------------------------------------------------------------- /api/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('api:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3001'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /api/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /api/routes/api.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var request = require('request'); 4 | var config = require('config'); 5 | var Datastore = require('nedb') 6 | , db = new Datastore({ filename: config.db_path, autoload: true }); 7 | 8 | /* GET saved url listing. */ 9 | router.get('/url', function (req, res, next) { 10 | // The same rules apply when you want to only find one document 11 | db.findOne({ name: 'url' }, function (err, doc) { 12 | if (err) { 13 | return res.status(500).json(err); 14 | } 15 | 16 | if (!doc) { 17 | var Docker = require('dockerode'); 18 | var docker = new Docker({ socketPath: '/var/run/docker.sock' }); 19 | docker.listContainers(function (err, containers) { 20 | if (err || !containers) { 21 | return res.status(404).json({ 22 | message: 'traefik url not provided' 23 | }); 24 | } else { 25 | var traefik = containers.filter((c) => { 26 | return c.Image == 'traefik' && c.State == 'running'; 27 | }) 28 | var ip; 29 | if (traefik.length) { 30 | var network = traefik[0].HostConfig.NetworkMode; 31 | if (network) { 32 | ip = traefik[0].NetworkSettings.Networks[network].IPAddress; 33 | } 34 | } 35 | } 36 | 37 | if (ip) { 38 | return res.json({ 39 | status: 'ok', 40 | url: 'http://' + ip + ':8080' 41 | }) 42 | } 43 | return res.status(404).json({ 44 | message: 'traefik url not provided' 45 | }); 46 | }); 47 | } else { 48 | 49 | return res.json({ 50 | status: 'ok', 51 | url: doc.value 52 | }) 53 | } 54 | }); 55 | }); 56 | 57 | /* GET users listing. */ 58 | router.put('/url', function (req, res, next) { 59 | var url = req.body.url; 60 | if (!url) { 61 | return res.status(400).json({ 62 | message: 'Incorrect url' 63 | }) 64 | } 65 | 66 | db.update({ name: 'url' }, { name: 'url', value: url }, { upsert: true }, function (err, num, newDoc) { 67 | if (err) { 68 | return res.status(500).json(err); 69 | } 70 | return res.json({ 71 | status: 'ok', 72 | url: url 73 | }) 74 | }); 75 | }); 76 | 77 | /* GET providers info from traefik. */ 78 | router.get('/providers', function (req, res, next) { 79 | db.findOne({ name: 'url' }, function (err, doc) { 80 | if (err) { 81 | return res.status(500).json(err); 82 | } 83 | 84 | if (!doc) { 85 | return res.status(404).json({ 86 | message: 'traefik url not provided' 87 | }); 88 | } 89 | 90 | var x = request(doc.value + '/api/providers', function (err, response) { 91 | if (err) { 92 | return res.status(500).json(err); 93 | } 94 | }); 95 | req.pipe(x) 96 | x.pipe(res) 97 | }); 98 | }); 99 | 100 | 101 | module.exports = router; 102 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_path": "./data/cache.db" 3 | } -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_path": "./test/data/cache.db" 3 | } -------------------------------------------------------------------------------- /data/cache.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumejacquart/react-traefik/9e05c7fab8ed33ce5a0138f6d34e9c342b6d8aee/data/cache.db -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-traefik", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "body-parser": "~1.17.1", 7 | "config": "^1.26.1", 8 | "cors": "^2.8.3", 9 | "d3": "^4.8.0", 10 | "debug": "~2.6.3", 11 | "dockerode": "^2.4.3", 12 | "express": "~4.15.2", 13 | "isomorphic-fetch": "^2.2.1", 14 | "jquery": "^3.2.1", 15 | "lodash": "^4.17.4", 16 | "morgan": "~1.8.1", 17 | "nedb": "^1.8.0", 18 | "prop-types": "^15.5.8", 19 | "react": "^15.5.4", 20 | "react-dom": "^15.5.4", 21 | "react-redux": "^5.0.4", 22 | "redux": "^3.6.0", 23 | "redux-logger": "^3.0.1", 24 | "redux-thunk": "^2.2.0", 25 | "request": "^2.81.0", 26 | "serve-favicon": "~2.4.2" 27 | }, 28 | "devDependencies": { 29 | "chai": "^3.5.0", 30 | "chai-http": "^3.0.0", 31 | "mocha": "^3.4.1", 32 | "react-scripts": "0.9.5", 33 | "should": "^11.2.1", 34 | "supertest": "^3.0.0" 35 | }, 36 | "scripts": { 37 | "start": "npm run start-api", 38 | "test": "mocha --timeout 10000", 39 | "start-api": "node ./api/bin/www", 40 | "start-front": "react-scripts start", 41 | "build-front": "react-scripts build", 42 | "test-front": "react-scripts test --env=jsdom", 43 | "eject-front": "react-scripts eject", 44 | "install": "npm run build-front" 45 | }, 46 | "proxy": "http://localhost:3001" 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumejacquart/react-traefik/9e05c7fab8ed33ce5a0138f6d34e9c342b6d8aee/public/favicon.ico -------------------------------------------------------------------------------- /public/images/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumejacquart/react-traefik/9e05c7fab8ed33ce5a0138f6d34e9c342b6d8aee/public/images/cloud.png -------------------------------------------------------------------------------- /public/images/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumejacquart/react-traefik/9e05c7fab8ed33ce5a0138f6d34e9c342b6d8aee/public/images/docker.png -------------------------------------------------------------------------------- /public/images/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumejacquart/react-traefik/9e05c7fab8ed33ce5a0138f6d34e9c342b6d8aee/public/images/file.png -------------------------------------------------------------------------------- /public/images/traefik.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumejacquart/react-traefik/9e05c7fab8ed33ce5a0138f6d34e9c342b6d8aee/public/images/traefik.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Traefik React D3 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-intro { 18 | font-size: large; 19 | } 20 | 21 | @keyframes App-logo-spin { 22 | from { transform: rotate(0deg); } 23 | to { transform: rotate(360deg); } 24 | } 25 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import Flow from './components/Flow.js'; 4 | import './App.css'; 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 |
10 |
11 | logo 12 |

Welcome to React

13 |
14 |

15 | To get started, edit src/App.js and save to reload. 16 |

17 | 18 |
19 | ); 20 | } 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/Conf.js: -------------------------------------------------------------------------------- 1 | //export const API_URL = "https://demo6651367.mockable.io" 2 | export const API_URL = "" -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch' 2 | import { API_URL } from '../Conf' 3 | 4 | export const REQUEST_CONFIG = 'REQUEST_CONFIG' 5 | export const RECEIVE_CONFIG = 'RECEIVE_CONFIG' 6 | export const SET_URL = 'SET_URL' 7 | export const REQUEST_TRAEFIK_PROVIDERS = 'REQUEST_TRAEFIK_PROVIDERS' 8 | export const RECEIVE_TRAEFIK_PROVIDERS = 'RECEIVE_TRAEFIK_PROVIDERS' 9 | export const INVALIDATE_DATA = 'INVALIDATE_DATA' 10 | export const SEARCH = 'SEARCH' 11 | 12 | export function invalidateData() { 13 | return { 14 | type: INVALIDATE_DATA 15 | } 16 | } 17 | 18 | function requestConfig() { 19 | return { 20 | type: REQUEST_CONFIG 21 | } 22 | } 23 | 24 | function receiveConfig(json) { 25 | return { 26 | type: RECEIVE_CONFIG, 27 | data: json 28 | } 29 | } 30 | 31 | function requestTraefikProviders() { 32 | return { 33 | type: REQUEST_TRAEFIK_PROVIDERS 34 | } 35 | } 36 | 37 | function receiveTraefikProviders(json) { 38 | return { 39 | type: RECEIVE_TRAEFIK_PROVIDERS, 40 | data: json, 41 | receivedAt: Date.now() 42 | } 43 | } 44 | 45 | export function setUrl(url) { 46 | return dispatch => { 47 | return dispatch({ 48 | type: SET_URL, 49 | data: url 50 | }) 51 | } 52 | } 53 | 54 | export function search(query) { 55 | return dispatch => { 56 | return dispatch({ 57 | type: SEARCH, 58 | data: query 59 | }) 60 | } 61 | } 62 | 63 | export function fetchConfigReady() { 64 | return dispatch => { 65 | dispatch(requestConfig()) 66 | return fetch(`${API_URL}/api/url`) 67 | .then(response => response.json()) 68 | .then((json) => { 69 | if (json.status === 'ok') { 70 | return dispatch(receiveConfig({ 71 | configReady: true, 72 | traefik_url: json.url 73 | })); 74 | } 75 | return dispatch(receiveConfig({ 76 | configReady: false, 77 | error: json 78 | })); 79 | }) 80 | .catch(function (error) { 81 | dispatch(receiveConfig({ 82 | configReady: false, 83 | error: error 84 | })) 85 | }); 86 | } 87 | } 88 | 89 | export function fetchProviders(traefik_url) { 90 | return dispatch => { 91 | dispatch(requestTraefikProviders()) 92 | return fetch(`${API_URL}/api/url`, { 93 | method: 'PUT', 94 | headers: { 95 | 'Content-Type': 'application/json' 96 | }, 97 | body: JSON.stringify({ 98 | url: traefik_url 99 | }) 100 | }) 101 | .then(response => response.json()) 102 | .then((json) => { 103 | if (json.status === 'ok') { 104 | return fetchProvidersData(dispatch); 105 | } 106 | throw new Error(json) 107 | }) 108 | .catch(function (error) { 109 | dispatch(receiveConfig({ 110 | configReady: false, 111 | error: error 112 | })) 113 | }); 114 | } 115 | } 116 | 117 | function fetchProvidersData(dispatch) { 118 | return fetch(`${API_URL}/api/providers`) 119 | .then(response => response.json()) 120 | .catch(function (error) { 121 | dispatch(receiveConfig({ 122 | configReady: false 123 | })) 124 | }) 125 | .then(json => { 126 | if(json.errno){ 127 | return dispatch(receiveConfig({ 128 | configReady: false, 129 | error: json 130 | })) 131 | } 132 | return dispatch(receiveTraefikProviders(json)) 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /src/components/Search.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class Search extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = {value: ''}; 9 | this.handleChange = this.handleChange.bind(this); 10 | } 11 | 12 | componentDidUpdate(){ 13 | } 14 | 15 | handleChange(event) { 16 | const { onChange } = this.props; 17 | this.setState({value: event.target.value}); 18 | onChange(event.target.value); 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 |
25 | 26 | 27 |
28 |
29 | ) 30 | } 31 | } 32 | 33 | Search.propTypes = { 34 | onChange: PropTypes.func.isRequired 35 | } -------------------------------------------------------------------------------- /src/components/ThreeJSFlow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import _ from 'lodash'; 3 | import Tree from './Tree' 4 | import * as d3 from 'd3' 5 | 6 | 7 | export default class ThreeJSFlow extends Component { 8 | 9 | loadData(props) { 10 | var jsonData = { 11 | name: "traefik", 12 | hasDetails: false, 13 | routes: [], 14 | children: [], 15 | image: { 16 | src: 'images/traefik.png', 17 | width: 100, 18 | height: 100 19 | }, 20 | className: 'traefik-root' 21 | }; 22 | 23 | var routesData = { 24 | name: "internet", 25 | hasDetails: false, 26 | routes: [], 27 | children: [], 28 | image: { 29 | src: 'images/cloud.png', 30 | width: 100, 31 | height: 100 32 | }, 33 | className: 'internet-root' 34 | } 35 | 36 | d3.selectAll('#d3-flow-svg *').remove(); 37 | var backendsDict = {}; 38 | var traefikLeaves = 0; 39 | 40 | for (var p in this.props.data.providers) { 41 | var provider = { 42 | name: p, 43 | hasDetails: false, 44 | routes: [], 45 | children: [], 46 | image: { 47 | src: 'images/' + p + '.png', 48 | width: p === "file" ? 75 : 100, 49 | height: p === "file" ? 75 : 100 50 | } 51 | } 52 | 53 | for (var b in this.props.data.providers[p].backends) { 54 | var urls = []; 55 | for (var s in this.props.data.providers[p].backends[b].servers) { 56 | var url = this.props.data.providers[p].backends[b].servers[s].url; 57 | var weight = this.props.data.providers[p].backends[b].servers[s].weight; 58 | urls.push({ 59 | name: s, 60 | hasDetails: true, 61 | details: '
  • Weight: ' + weight + '
  • URL: ' + url + '
', 62 | depth: 150, 63 | width: 200, 64 | className: "traefik-server" 65 | }); 66 | 67 | } 68 | var backendData = { 69 | name: b, 70 | urls: urls, 71 | children: urls 72 | }; 73 | backendsDict[b] = backendData; 74 | traefikLeaves++; 75 | } 76 | 77 | for (var f in this.props.data.providers[p].frontends) { 78 | var entrypoints = this.props.data.providers[p].frontends[f].entryPoints.join(", ") 79 | var backend = this.props.data.providers[p].frontends[f].backend; 80 | 81 | var routes = []; 82 | for (var r in this.props.data.providers[p].frontends[f].routes) { 83 | var route = { 84 | name: r, 85 | value: this.props.data.providers[p].frontends[f].routes[r].rule 86 | }; 87 | var scheme = entrypoints.includes('https') ? 'https://' : 'http://'; 88 | var url = scheme + route.value.split(":")[1]; 89 | routes.push(route); 90 | routesData.routes.push(route); 91 | routesData.children.push({ 92 | name: route.name, 93 | route: route, 94 | entryPoints: entrypoints, 95 | backend: backend, 96 | hasDetails: true, 97 | details: '
' + route.value + '
' 98 | }) 99 | } 100 | 101 | backendsDict[backend].children.forEach((c) => { 102 | c.routes = routes; 103 | }); 104 | provider.children.push({ 105 | name: backend, 106 | urls: backendsDict[backend].urls, 107 | details: backendsDict[backend].details, 108 | children: backendsDict[backend].children, 109 | routes:routes 110 | }); 111 | 112 | provider.routes = provider.routes.concat(routes); 113 | jsonData.routes = jsonData.routes.concat(routes); 114 | }; 115 | 116 | jsonData.children.push(provider) 117 | } 118 | 119 | var tree = new Tree(); 120 | tree.createTree("#d3-flow-svg", jsonData, "left-to-right", traefikLeaves * 150); 121 | tree.createTree("#d3-flow-svg", routesData, "right-to-left", routesData.children.length * 170); 122 | // 123 | } 124 | 125 | componentDidMount() { 126 | this.loadData(this.props); 127 | } 128 | 129 | componentDidUpdate(prevProps) { 130 | if (_.isEqual(prevProps.data.providers, this.props.data.providers)) { 131 | return; 132 | } 133 | 134 | this.loadData(this.props); 135 | } 136 | 137 | render() { 138 | return ( 139 |
140 | 141 |
142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/components/Tree.js: -------------------------------------------------------------------------------- 1 | import * as d3 from "d3"; 2 | import $ from 'jquery'; 3 | 4 | export default class Tree { 5 | constructor() { 6 | this.svg = null; 7 | this.svgContainer = null; 8 | this.root1 = null; 9 | this.root2 = null; 10 | } 11 | 12 | createTree(selector, data, orient, height) { 13 | // Set the dimensions and margins of the diagram 14 | var margin = { 15 | top: 0, 16 | right: 0, 17 | bottom: 0, 18 | left: 0 19 | }, 20 | initWidth = Math.max(document.getElementById('d3-flow').clientWidth - 40, 1300), 21 | initHeight = height, 22 | depth = 180; 23 | 24 | console.log(initHeight); 25 | var orient = orient || "left-to-right"; 26 | var coeff = orient == "left-to-right" ? 1 : -1; 27 | margin.left = (initWidth) / 2 + coeff*50 + (coeff == -1 ? -1 * depth / 2 : -depth / 2); 28 | 29 | var width = initWidth - margin.left - margin.right; 30 | var height = initHeight - margin.top - margin.bottom; 31 | 32 | // append the svg object to the body of the page 33 | // appends a 'group' element to 'svg' 34 | // moves the 'group' element to the top left margin 35 | 36 | this.svgContainer = d3.select(selector); 37 | 38 | this.svg = this.svgContainer 39 | .attr("width", width + margin.right + margin.left) 40 | .attr("height", height + margin.top + margin.bottom) 41 | .append("g") 42 | .attr("transform", "translate(" + 43 | margin.left + "," + margin.top + ")"); 44 | 45 | var svg = this.svg; 46 | 47 | var drag = d3.drag() 48 | .on("start", dragstarted) 49 | .on("drag", dragged) 50 | .on("end", dragended); 51 | d3.select(selector).call(drag); 52 | 53 | function dragstarted(d) { 54 | d3.event.sourceEvent.stopPropagation(); 55 | d3.select(this).classed("dragging", true); 56 | } 57 | 58 | function dragged(d) { 59 | var dxString = d3.select(this).attr("cx") || '0'; 60 | var dyString = d3.select(this).attr("cy") || '0'; 61 | var dx = parseFloat(dxString); 62 | var dy = parseFloat(dyString); 63 | var x0 = dx + d3.event.dx; 64 | var y0 = dy + d3.event.dy; 65 | 66 | d3.select(this).attr("cx", x0); 67 | d3.select(this).attr("cy", y0); 68 | d3.select(this).attr("transform", "translate(" + x0 + "," + y0 + ")"); 69 | } 70 | 71 | function dragended(d) { /**/ 72 | d3.select(this).classed("dragging", false); 73 | } 74 | 75 | var i = 0, 76 | duration = 350, 77 | root; 78 | 79 | // declares a tree layout and assigns the size 80 | var treemap = d3.tree().size([height, width]); 81 | 82 | // Assigns parent, children, height, depth 83 | root = d3.hierarchy(data, function(d) { 84 | return d.children; 85 | }); 86 | root.x0 = width / 2; 87 | root.y0 = height / 2; 88 | 89 | // Collapse after the second level 90 | //root.children.forEach(collapse); 91 | 92 | update(root); 93 | 94 | function getClassName(constant, d) { 95 | var className = constant 96 | if (d.data.route) { 97 | className += ' ' + d.data.route.name 98 | } 99 | if (d.data.routes) { 100 | className += ' ' + d.data.routes.map((r) => r.name).join(' ') 101 | } 102 | if(d.data.className){ 103 | className += ' ' + d.data.className 104 | } 105 | return className; 106 | } 107 | 108 | function update(source) { 109 | 110 | // Assigns the x and y position for the nodes 111 | var treeData = treemap(root); 112 | 113 | // Compute the new tree layout. 114 | var nodes = treeData.descendants(), 115 | links = treeData.descendants().slice(1); 116 | 117 | // Normalize for fixed-depth. 118 | nodes.forEach(function(d) { 119 | d.y = d.depth * (d.data.depth || depth) 120 | }); 121 | 122 | // ****************** Nodes section *************************** 123 | 124 | // Update the nodes... 125 | var node = svg.selectAll('g.node') 126 | .data(nodes, function(d) { 127 | return d.id || (d.id = ++i); 128 | }); 129 | 130 | // Enter any new modes at the parent's previous position. 131 | var nodeEnter = node.enter().append('g') 132 | .attr('class', function(d) { 133 | return getClassName('node', d); 134 | }) 135 | .attr("transform", function(d) { 136 | return "translate(" + source.y0 + "," + source.x0 + ")"; 137 | }) 138 | .on('click', click); 139 | 140 | // Add Circle for the nodes 141 | nodeEnter.append('circle') 142 | .attr('class', function(d) { 143 | var className = 'node' 144 | if (d.data.rule) { 145 | className += ' ' + d.data.rule 146 | } 147 | if (d.data.rules) { 148 | className += ' ' + d.data.rules.join(' ') 149 | } 150 | return className; 151 | }) 152 | .attr('r', function(d){ 153 | return d.data.image ? 0 : 1e-6 154 | }); 155 | 156 | // Add HTML for the nodes 157 | var nodeHtml = nodeEnter.append('foreignObject') 158 | .attr('class', function(d) { 159 | return getClassName('node-html-wrapper', d); 160 | }) 161 | .attr("width", function(d) { 162 | return d.data.hasDetails ? (d.data.width || "250") : "80"; 163 | }) 164 | .attr("x", function(d) { 165 | return d.children || d._children ? coeff * -100 : (coeff == -1 ? -270 : 20); 166 | }) 167 | .attr("y", "-2.7em") 168 | .on('mouseover', function(d) { 169 | var classes = d3.select(this).attr('class') 170 | .replace('node-html-wrapper', '') 171 | .replace(' traefik-server', '') 172 | .split(" ") 173 | .join(', .'); 174 | if (classes[0] == ',') { 175 | classes = classes.replace(', ', ''); 176 | } 177 | d3.selectAll(classes).classed('node-active', true); 178 | }) 179 | .on('mouseleave', function(d) { 180 | var classes = d3.select(this).attr('class') 181 | .replace('node-html-wrapper', '') 182 | .replace(' traefik-server', '') 183 | .split(" ") 184 | .join(', .'); 185 | if (classes[0] == ',') { 186 | classes = classes.replace(', ', ''); 187 | } 188 | d3.selectAll(classes).classed('node-active', false); 189 | }); 190 | 191 | nodeHtml.append('xhtml:div') 192 | .attr('class', 'node-html') 193 | .html(function(d) { 194 | return '
' + d.data.name + '
' + 195 | '
' + (d.data.details || '') + '
'; 196 | }); 197 | 198 | nodeHtml.filter(function(d) { 199 | return !d.data.hasDetails 200 | }).remove() 201 | 202 | // Add labels for the nodes 203 | var nodeText = nodeEnter.filter(function(d) { 204 | return !d.data.hasDetails && !d.data.image 205 | }).append('text') 206 | .attr('class', function(d) { 207 | return getClassName('node-text', d); 208 | }) 209 | .attr("dy", "-1.5em") 210 | .attr("x", function(d) { 211 | return 0//d.children || d._children ? coeff * -13 : coeff * 13; 212 | }) 213 | .attr("text-anchor", function(d) { 214 | return "middle";//d.children || d._children ? (coeff == 1 ? "end" : "start") : (coeff == 1 ? "start" : "end"); 215 | }) 216 | .text(function(d) { 217 | return d.data.name; 218 | }); 219 | 220 | // Add labels for the nodes 221 | var nodeImages = nodeEnter.filter(function(d){ 222 | return d.data.image; 223 | }).append("image") 224 | .attr('xlink:href', function(d){ 225 | return d.data.image.src; 226 | }) 227 | .attr('cursor', 'pointer') 228 | .attr('height', function(d){ 229 | return d.data.image.height; 230 | }) 231 | .attr('width', function(d){ 232 | return d.data.image.width; 233 | }) 234 | .attr('class', function(d) { 235 | return getClassName('node-image', d); 236 | }) 237 | .attr("y", function(d) { 238 | return -d.data.image.height / 2 + 'px' 239 | }) 240 | .attr("x", function(d) { 241 | var x = -d.data.image.width / 2; 242 | return x; 243 | }); 244 | 245 | nodeImages.filter(function(d) { 246 | return !d.data.image 247 | }).remove() 248 | 249 | // UPDATE 250 | var nodeUpdate = nodeEnter.merge(node); 251 | 252 | // Transition to the proper position for the node 253 | nodeUpdate.transition() 254 | .duration(duration) 255 | .attr("transform", function(d) { 256 | return "translate(" + coeff * d.y + "," + d.x + ")"; 257 | }) 258 | .on('end', (e) => { 259 | if(e.data.className === 'internet-root'){ 260 | var top1 = $('.traefik-root').position().top; 261 | var top2 = $('.internet-root').position().top; 262 | svg.attr("transform", "translate(" + margin.left + "," + (margin.top + (top1-top2)/2) + ")"); 263 | } 264 | });; 265 | 266 | // Update the node attributes and style 267 | nodeUpdate.select('circle.node') 268 | .attr('r', function(d){ 269 | return d.data.image ? 1e-6 : 10 270 | }) 271 | .attr('cursor', 'pointer'); 272 | 273 | 274 | // Remove any exiting nodes 275 | var nodeExit = node.exit().transition() 276 | .duration(duration) 277 | .attr("opacity", 0) 278 | .attr("transform", function(d) { 279 | return "translate(" + source.y + "," + source.x + ")"; 280 | }) 281 | .remove(); 282 | 283 | // On exit reduce the node circles size to 0 284 | nodeExit.select('circle') 285 | .attr('r', 1e-6); 286 | 287 | // On exit reduce the opacity of text labels 288 | nodeExit.select('text') 289 | .style('fill-opacity', 1e-6); 290 | 291 | // ****************** links section *************************** 292 | 293 | // Update the links... 294 | var link = svg.selectAll('path.link') 295 | .data(links, function(d) { 296 | return d.id; 297 | }); 298 | 299 | // Enter any new links at the parent's previous position. 300 | var linkEnter = link.enter().insert('path', "g") 301 | .attr('class', function(d) { 302 | return getClassName('link', d); 303 | }) 304 | .attr("id", function(d, i) { 305 | return 'path-' + d.data.name 306 | }) 307 | .attr('d', function(d) { 308 | var p = { 309 | x: source.x0, 310 | y: source.y0 311 | }; 312 | return diagonal(p, p) 313 | }); 314 | 315 | // UPDATE 316 | var linkUpdate = linkEnter.merge(link); 317 | 318 | // Transition back to the parent element position 319 | linkUpdate.transition() 320 | .duration(duration) 321 | .attr('d', function(d) { 322 | return diagonal(d, d.parent) 323 | }); 324 | 325 | // Remove any exiting links 326 | link.exit().transition() 327 | .duration(duration) 328 | .attr('d', function(d) { 329 | var p = { 330 | x: source.x, 331 | y: source.y 332 | } 333 | return diagonal(p, p) 334 | }) 335 | .remove(); 336 | 337 | // ****************** links text section *************************** 338 | 339 | // Update the links... 340 | var linkText = svg.selectAll('text.entrypoints') 341 | .data(links); 342 | 343 | // Add labels for the nodes 344 | var linkTextEnter = linkText.enter().append('text') 345 | .attr("dy", "-0.3em") 346 | .attr('class', function(d) { 347 | return getClassName('entrypoints', d); 348 | }) 349 | .attr("fill", "Black") 350 | .style("font", "normal 12px Arial") 351 | .append("textPath") 352 | .attr("startOffset", "55.76%") 353 | .style("text-anchor", "end") 354 | .attr("xlink:href", function(d, i) { 355 | return '#path-' + d.data.name; 356 | }) 357 | .text(function(d) { 358 | return d.data.entryPoints; 359 | }); 360 | 361 | // Remove any exiting links 362 | linkText.exit().remove(); 363 | 364 | // Store the old positions for transition. 365 | nodes.forEach(function(d) { 366 | d.x0 = d.x; 367 | d.y0 = d.y; 368 | }); 369 | 370 | // Creates a curved (diagonal) path from parent to the child nodes 371 | function diagonal(s, d) { 372 | var path = `M ${coeff*s.y} ${s.x} 373 | C ${coeff*(s.y + d.y) / 2} ${s.x}, 374 | ${coeff*(s.y + d.y) / 2} ${d.x}, 375 | ${d.y} ${d.x}` 376 | 377 | return path 378 | } 379 | 380 | // Toggle children on click. 381 | function click(d) { 382 | if (d.children) { 383 | d._children = d.children; 384 | d.children = null; 385 | } 386 | else { 387 | d.children = d._children; 388 | d._children = null; 389 | } 390 | update(d); 391 | } 392 | } 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/components/UrlInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | export default class UrlInput extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.handleChange = this.handleChange.bind(this); 9 | this.handleSubmit = this.handleSubmit.bind(this); 10 | } 11 | 12 | componentDidUpdate(prevProps){ 13 | if(this.props.value != prevProps.value){ 14 | this.setState({ value: this.props.value }); 15 | } 16 | } 17 | 18 | handleSubmit(event) { 19 | const { onClick } = this.props; 20 | onClick(this.state.value); 21 | event.preventDefault(); 22 | } 23 | 24 | handleChange(event) { 25 | this.setState({value: event.target.value}); 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 | 32 | 33 | 34 | 35 |
36 | ) 37 | } 38 | } 39 | 40 | UrlInput.propTypes = { 41 | value: PropTypes.string, 42 | onClick: PropTypes.func.isRequired 43 | } -------------------------------------------------------------------------------- /src/containers/AsyncApp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | import { fetchProviders, fetchConfigReady, setUrl, search } from '../actions' 5 | import UrlInput from '../components/UrlInput' 6 | import Search from '../components/Search' 7 | import ThreeJSFlow from '../components/ThreeJSFlow' 8 | 9 | class AsyncApp extends Component { 10 | timer = null 11 | 12 | constructor(props) { 13 | super(props) 14 | this.handleChange = this.handleChange.bind(this) 15 | this.handleSearchChange = this.handleSearchChange.bind(this) 16 | } 17 | 18 | loadData(dispatch, traefik_url) { 19 | dispatch(fetchProviders(traefik_url)) 20 | } 21 | 22 | componentDidMount() { 23 | const { dispatch } = this.props 24 | dispatch(fetchConfigReady()) 25 | } 26 | 27 | componentDidUpdate(prevProps) { 28 | const { dispatch, traefik_url } = this.props 29 | if(!this.props.traefikData){ 30 | return; 31 | } 32 | 33 | if (this.props.traefikData.traefik_url !== prevProps.traefikData.traefik_url) { 34 | if (this.props.traefikData.configReady) { 35 | if(!this.props.traefikData.providers){ 36 | this.loadData(dispatch, this.props.traefikData.traefik_url); 37 | } 38 | this.timer = window.setInterval(() => this.loadData(dispatch, this.props.traefikData.traefik_url), 15000); 39 | } 40 | } 41 | 42 | if(!this.props.traefikData.configReady && this.timer){ 43 | window.clearTimeout(this.timer); 44 | delete this.timer; 45 | } 46 | } 47 | 48 | handleChange(next_traefik_url) { 49 | const { dispatch } = this.props 50 | dispatch(setUrl(next_traefik_url)) 51 | } 52 | 53 | handleSearchChange(query) { 54 | const { dispatch } = this.props 55 | dispatch(search(query)) 56 | } 57 | 58 | render() { 59 | const { traefik_url, traefikData, isFetching, lastUpdatedProviders } = this.props 60 | return ( 61 |
62 | 78 |
79 | {traefikData.providers && 80 | 81 | } 82 |

83 | {lastUpdatedProviders && 84 | 85 | Last updated at {new Date(lastUpdatedProviders).toLocaleTimeString()}. 86 | {' '} 87 | 88 | } 89 |

90 | {isFetching && !traefikData.providers && 91 |

Loading...

92 | } 93 | {!isFetching && !traefikData.traefik_url && 94 |

Fill out your traefik url in the navigation header

95 | } 96 | {traefikData.error && !traefikData.providers && 97 |

{traefikData.error.message}

98 | } 99 |
100 |
101 | {traefikData.providers && 102 | 103 | } 104 |
105 |
106 | ) 107 | } 108 | } 109 | 110 | AsyncApp.propTypes = { 111 | traefikData: PropTypes.object, 112 | isFetching: PropTypes.bool.isRequired, 113 | lastUpdatedProviders: PropTypes.number, 114 | dispatch: PropTypes.func.isRequired 115 | } 116 | 117 | function mapStateToProps(state) { 118 | const { traefik_url, traefikData } = state 119 | const { 120 | isFetching, 121 | lastUpdatedProviders 122 | } = { 123 | isFetching: false 124 | } 125 | 126 | return { 127 | traefik_url, 128 | isFetching, 129 | lastUpdatedProviders, 130 | traefikData 131 | } 132 | } 133 | 134 | export default connect(mapStateToProps)(AsyncApp) -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { Provider } from 'react-redux' 3 | import configureStore from '../store/configureStore' 4 | import AsyncApp from './AsyncApp' 5 | 6 | const store = configureStore() 7 | 8 | export default class Root extends Component { 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html{ 2 | width: 100%; 3 | height: 100%; 4 | } 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | font-family: sans-serif; 9 | } 10 | 11 | .traefik-form-url{ 12 | margin-top: 15px; 13 | } 14 | 15 | .traefik-form-search{ 16 | margin: 10px 0; 17 | } 18 | 19 | #d3-flow-svg{ 20 | display: block; 21 | margin: auto; 22 | } 23 | 24 | .block{ 25 | font-size: 12px; 26 | word-break: break-word; 27 | padding:5px; 28 | } 29 | 30 | .block .name{ 31 | font-weight: bold; 32 | margin: -5px; 33 | padding: 5px; 34 | border-bottom: 1px solid #333; 35 | margin-bottom: 5px; 36 | } 37 | 38 | .backend .name{ 39 | background-color: rgba(100, 100, 255, 0.4); 40 | } 41 | 42 | .backend .urls{ 43 | padding-left:15px; 44 | list-style-type: circle; 45 | } 46 | 47 | .frontend .name{ 48 | background-color: rgba(100, 255,100, 0.4); 49 | } 50 | 51 | .frontend .routes{ 52 | padding-left:15px; 53 | list-style-type: circle; 54 | } 55 | 56 | .node circle { 57 | fill: #fff; 58 | stroke: steelblue; 59 | stroke-width: 3px; 60 | transition: all 0.2s; 61 | } 62 | 63 | .node text { 64 | font: 12px sans-serif; 65 | transition: all 0.2s; 66 | } 67 | 68 | .link { 69 | fill: none; 70 | stroke: #ccc; 71 | stroke-width: 2px; 72 | transition: stroke 0.2s, stroke-width 0.2s; 73 | } 74 | 75 | .node-html{ 76 | font-size:0.8rem; 77 | overflow: hidden; 78 | word-break: break-all; 79 | border: 2px solid steelblue; 80 | padding: 8px; 81 | box-shadow: 3px 2px 6px rgba(0,0,0,0.5); 82 | transition: all 0.2s; 83 | background-color: steelblue; 84 | color:#fff; 85 | } 86 | 87 | .node-html ul{ 88 | margin: 0; 89 | padding-left: 15px; 90 | font-size:0.95em; 91 | } 92 | 93 | .node-name{ 94 | margin: -8px -8px 0 -8px; 95 | padding: 8px; 96 | border-bottom: 1px solid #003b65; 97 | text-align: center; 98 | font-weight:bold; 99 | } 100 | 101 | .node-details, .node-details *{ 102 | font-weight:normal !important; 103 | } 104 | 105 | .backend-link{ 106 | color:white; 107 | cursor:pointer; 108 | } 109 | 110 | .backend-link:hover{ 111 | color:white; 112 | } 113 | 114 | .traefik-server .node-html{ 115 | background-color:#583; 116 | border-color:#583; 117 | } 118 | 119 | .node-active * { 120 | font-weight:bold; 121 | transition: all 0.2s; 122 | fill: #583; 123 | } 124 | 125 | .node-active .node-html{ 126 | border: 2px solid #583; 127 | } 128 | 129 | .node-active circle { 130 | fill: #fff; 131 | stroke: #583; 132 | stroke-width: 5; 133 | transition: all 0.2s; 134 | } 135 | 136 | .node text.node-active { 137 | font: 12px sans-serif; 138 | font-weight: bold; 139 | transition: all 0.2s; 140 | } 141 | 142 | .link.node-active { 143 | fill: none; 144 | stroke: #583; 145 | stroke-width: 4px; 146 | transition: stroke 0.2s, stroke-width 0.2s; 147 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import Root from './containers/Root' 4 | import './index.css'; 5 | 6 | render( 7 | , 8 | document.getElementById('root') 9 | ) -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import * as _ from 'lodash'; 3 | import { 4 | REQUEST_TRAEFIK_PROVIDERS, RECEIVE_TRAEFIK_PROVIDERS, 5 | REQUEST_CONFIG, RECEIVE_CONFIG, SET_URL, SEARCH 6 | } from '../actions' 7 | 8 | function data(state = { 9 | isFetching: false, 10 | providers: [], 11 | fetchedProviders: [] 12 | }, action) { 13 | var newProviders; 14 | switch (action.type) { 15 | case REQUEST_TRAEFIK_PROVIDERS: 16 | return Object.assign({}, state, { 17 | isFetching: true, 18 | didInvalidate: false 19 | }) 20 | case RECEIVE_TRAEFIK_PROVIDERS: 21 | newProviders = filterProviders(action.data, state.search_query || ''); 22 | return Object.assign({}, state, { 23 | isFetching: false, 24 | didInvalidate: false, 25 | providers: newProviders, 26 | error: false, 27 | fetchedProviders: action.data, 28 | lastUpdatedProviders: action.receivedAt 29 | }) 30 | case REQUEST_CONFIG: 31 | return Object.assign({}, state, { 32 | isFetching: true, 33 | didInvalidate: false 34 | }) 35 | case RECEIVE_CONFIG: 36 | var newState = { 37 | configReady: action.data.configReady, 38 | error: action.data.error, 39 | traefik_url: action.data.traefik_url 40 | } 41 | if(!action.data){ 42 | newState.providers = false; 43 | } 44 | return Object.assign({}, state, newState) 45 | case SET_URL: 46 | return Object.assign({}, state, { 47 | traefik_url: action.data, 48 | providers: false, 49 | configReady: true 50 | }) 51 | case SEARCH: 52 | newProviders = filterProviders(state.fetchedProviders, action.data); 53 | return Object.assign({}, state, { 54 | search_query: action.data, 55 | providers: newProviders 56 | }) 57 | default: 58 | return state 59 | } 60 | } 61 | 62 | function filterProviders(oldProviders, query){ 63 | if(!query.length){ 64 | return oldProviders; 65 | } 66 | 67 | var newProviders = _.cloneDeep(oldProviders); 68 | var backendsFound = []; 69 | var frontendFounds = []; 70 | var serversFounds = []; 71 | 72 | for(var conf in oldProviders){ 73 | for(var b in oldProviders[conf].backends){ 74 | if(b.includes(query)){ 75 | backendsFound.push(b); 76 | serversFounds = serversFounds.concat(Object.keys(oldProviders[conf].backends[b].servers)); 77 | continue; 78 | } 79 | 80 | for(var s in oldProviders[conf].backends[b].servers){ 81 | if(oldProviders[conf].backends[b].servers[s].url.includes(query)){ 82 | backendsFound.push(b); 83 | serversFounds.push(s); 84 | continue; 85 | } 86 | } 87 | } 88 | for(var f in oldProviders[conf].frontends){ 89 | var fBackend = oldProviders[conf].frontends[f].backend; 90 | if(f.includes(query)){ 91 | frontendFounds.push(f); 92 | backendsFound.push(fBackend); 93 | continue; 94 | } 95 | 96 | if(backendsFound.indexOf(fBackend) >= 0){ 97 | frontendFounds.push(f); 98 | backendsFound.push(fBackend); 99 | } 100 | 101 | for(var r in oldProviders[conf].frontends[f].routes){ 102 | if(oldProviders[conf].frontends[f].routes[r].rule.includes(query)){ 103 | frontendFounds.push(f); 104 | backendsFound.push(fBackend); 105 | serversFounds = serversFounds.concat(Object.keys(oldProviders[conf].backends[fBackend].servers)); 106 | continue; 107 | } 108 | } 109 | } 110 | } 111 | 112 | for(var conf in oldProviders){ 113 | for(var b in oldProviders[conf].backends){ 114 | if(backendsFound.indexOf(b) < 0 ){ 115 | delete newProviders[conf].backends[b]; 116 | continue; 117 | } 118 | 119 | for(var s in oldProviders[conf].backends[b].servers){ 120 | if(serversFounds.indexOf(s) < 0){ 121 | delete newProviders[conf].backends[b].servers[s]; 122 | } 123 | } 124 | } 125 | 126 | for(var f in oldProviders[conf].frontends){ 127 | if(frontendFounds.indexOf(f) < 0 ){ 128 | delete newProviders[conf].frontends[f]; 129 | } 130 | } 131 | 132 | if(!Object.keys(newProviders[conf].backends).length){ 133 | delete newProviders[conf]; 134 | } 135 | } 136 | return newProviders; 137 | } 138 | 139 | function traefikData(state = { }, action) { 140 | switch (action.type) { 141 | case REQUEST_TRAEFIK_PROVIDERS: 142 | case RECEIVE_TRAEFIK_PROVIDERS: 143 | case REQUEST_CONFIG: 144 | case RECEIVE_CONFIG: 145 | case SET_URL: 146 | case SEARCH: 147 | return Object.assign({}, state, data(state, action)) 148 | default: 149 | return state 150 | } 151 | } 152 | 153 | const rootReducer = combineReducers({ 154 | traefikData 155 | }) 156 | 157 | export default rootReducer -------------------------------------------------------------------------------- /src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux' 2 | import thunkMiddleware from 'redux-thunk' 3 | import { createLogger } from 'redux-logger' 4 | import rootReducer from '../reducers' 5 | 6 | const loggerMiddleware = createLogger() 7 | 8 | export default function configureStore(preloadedState) { 9 | return createStore( 10 | rootReducer, 11 | preloadedState, 12 | applyMiddleware( 13 | thunkMiddleware, 14 | loggerMiddleware 15 | ) 16 | ) 17 | } -------------------------------------------------------------------------------- /test/data/cache.db: -------------------------------------------------------------------------------- 1 | {"name":"url","value":"https://demo6651367.mockable.io","_id":"QKFLV14BHDxbe2hw"} 2 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | //During the test the env variable is set to test 2 | process.env.NODE_ENV = 'test'; 3 | //Require the dev-dependencies 4 | let chai = require('chai'); 5 | let chaiHttp = require('chai-http'); 6 | let server = require('../api/app.js'); 7 | let should = chai.should(); 8 | var config = require('config'); 9 | var fs = require('fs'); 10 | 11 | chai.use(chaiHttp); 12 | //Our parent block 13 | describe('API', () => { 14 | before((done) => { //Before each test we empty the database 15 | if(fs.existsSync(config.db_path)){ 16 | fs.unlinkSync(config.db_path); 17 | } 18 | return done(); 19 | }); 20 | 21 | /* 22 | * Test the /GET api/url before filling it 23 | */ 24 | describe('/GET api/url', () => { 25 | it('should return 404', (done) => { 26 | chai.request(server) 27 | .get('/api/url') 28 | .end((err, res) => { 29 | res.should.have.status(404); 30 | res.body.should.be.a('object'); 31 | res.body.should.have.deep.property('message'); 32 | // 33 | done(); 34 | }); 35 | }); 36 | }); 37 | 38 | 39 | /* 40 | * Test the /PUT api/url filling and getting 41 | */ 42 | describe('/PUT api/url', () => { 43 | it('should return 200', (done) => { 44 | chai.request(server) 45 | .put('/api/url') 46 | .send({ url: 'https://demo6651367.mockable.io' }) 47 | .end((err, res) => { 48 | console.log(err); 49 | res.should.have.status(200); 50 | res.body.should.be.a('object'); 51 | res.body.should.have.deep.property('url'); 52 | // 53 | done(); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('/GET api/url', () => { 59 | it('should return 200', (done) => { 60 | chai.request(server) 61 | .get('/api/url') 62 | .end((err, res) => { 63 | console.log(err); 64 | res.should.have.status(200); 65 | res.body.should.be.a('object'); 66 | res.body.should.have.deep.property('url'); 67 | // 68 | done(); 69 | }); 70 | }); 71 | }); 72 | 73 | /* 74 | * Test the /GET api/providers before filling it 75 | */ 76 | describe('/GET api/providers', () => { 77 | it('should return 200', (done) => { 78 | chai.request(server) 79 | .get('/api/providers') 80 | .end((err, res) => { 81 | console.log(err); 82 | res.should.have.status(200); 83 | res.body.should.be.a('object'); 84 | res.body.should.have.deep.property('docker.backends.backend-portainer.servers.server-portainer.url'); 85 | // 86 | done(); 87 | }); 88 | }); 89 | }); 90 | 91 | }); 92 | return; --------------------------------------------------------------------------------