├── .gitignore ├── LICENSE ├── README.md ├── bin.js ├── client ├── bar.css ├── details.js ├── fill.js ├── hash.js ├── index.js ├── info.css ├── legend.css ├── legend.js ├── mouseOver.js ├── ring.css └── text.js ├── control.png ├── index.js ├── package.json └── upring.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Matteo Collina 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # upring-control 2 | 3 | [![npm version][npm-badge]][npm-url] 4 | 5 | Control panel for [UpRing][upring]. 6 | 7 | ![screenshot][screenshot-url] 8 | 9 | ## Install 10 | 11 | ``` 12 | npm i upring-control pino -g 13 | ``` 14 | 15 | ## Usage 16 | 17 | ```sh 18 | upring-control 192.168.1.185:7979 # adjust to an ip of one of your hosts 19 | ``` 20 | 21 | **upring-control** can also act as a base swim node if needed. 22 | 23 | ### Full help 24 | 25 | ``` 26 | Usage: upring-control [OPTS] [BASE] 27 | 28 | Options: 29 | -t/--timeout MILLIS milliseconds to wait to join the ring, default 200 30 | -p/--port PORT the port on which the control panel will be exposes, default 8008 31 | -P/--points the number of points for each peer, default 100 32 | -v/--verbose enable debug logs 33 | -V/--version print the version number 34 | -h/--help shows this help 35 | ``` 36 | 37 | 38 | ## Acknowledgements 39 | 40 | This project is kindly sponsored by [nearForm](http://nearform.com). 41 | 42 | ## License 43 | 44 | MIT 45 | 46 | [upring]: https://github.com/mcollina/upring 47 | [logo-url]: https://raw.githubusercontent.com/mcollina/upring/master/upring.png 48 | [screenshot-url]: https://raw.githubusercontent.com/mcollina/upring-control/master/control.png 49 | [npm-badge]: https://badge.fury.io/js/upring-control.svg 50 | [npm-url]: https://badge.fury.io/js/upring-control 51 | -------------------------------------------------------------------------------- /bin.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | 'use strict' 4 | 5 | const control = require('.') 6 | const UpRing = require('upring') 7 | const args = require('minimist')(process.argv.slice(2), { 8 | alias: { 9 | timeout: 't', 10 | port: 'p', 11 | points: 'P', 12 | help: 'h', 13 | version: 'V', 14 | verbose: 'v' 15 | }, 16 | default: { 17 | points: 100, 18 | timeout: 200, 19 | port: 8008 20 | } 21 | }) 22 | 23 | if (args.help) { 24 | console.log(`Usage: upring-control [OPTS] [BASE] 25 | 26 | Options: 27 | -t/--timeout MILLIS milliseconds to wait to join the ring, default 200 28 | -p/--port PORT the port on which the control panel will be exposes, default 8008 29 | -P/--points the number of points for each peer, default 100 30 | -v/--verbose enable debug logs 31 | -V/--version print the version number 32 | -h/--help shows this help 33 | `) 34 | process.exit(1) 35 | } 36 | 37 | if (args.version) { 38 | console.log('v' + require('./package').version) 39 | process.exit(1) 40 | } 41 | 42 | const pinoHttp = require('pino-http') 43 | 44 | const upring = UpRing({ 45 | client: true, // this does not provides services to the ring 46 | logLevel: args.verbose ? 'debug' : 'info', 47 | hashring: { 48 | replicaPoints: args.points, 49 | joinTimeout: args.timeout, 50 | base: args._ 51 | } 52 | }) 53 | 54 | const logger = pinoHttp({ logger: upring.logger }) 55 | 56 | const server = control(upring) 57 | 58 | upring.on('up', () => { 59 | server.on('request', logger) 60 | server.listen(args.port, function () { 61 | logger.logger.info('server listening on port %d', this.address().port) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /client/bar.css: -------------------------------------------------------------------------------- 1 | 2 | :host { 3 | position: absolute; 4 | left: 0px; 5 | right: 0px; 6 | } 7 | 8 | .logo { 9 | position: absolute; 10 | left: 0px; 11 | top: 0px; 12 | height:7em; 13 | margin-left: 0.1em; 14 | margin-top: 0.1em; 15 | } 16 | 17 | .inputBox { 18 | background-color: #F1F1F1; 19 | padding: 1em; 20 | margin-right: 2em; 21 | position: absolute; 22 | right: 0px; 23 | top: 0px; 24 | border-radius: 0 0 0.5em 0.5em; 25 | } 26 | -------------------------------------------------------------------------------- /client/details.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const xhr = require('xhr') 4 | const yo = require('yo-yo') 5 | const sheetify = require('sheetify') 6 | const style = sheetify('./info.css') 7 | 8 | function build (svg) { 9 | var elem = render() 10 | var lastId = null 11 | 12 | document.body.appendChild(elem) 13 | 14 | return function onclick (ev) { 15 | const id = ev.data.id 16 | 17 | if (lastId === id) { 18 | hide() 19 | return 20 | } 21 | 22 | xhr.get(`/peer/${id}`, function (err, res, body) { 23 | if (err) { 24 | console.log(err) 25 | return 26 | } 27 | lastId = id 28 | elem = yo.update(elem, render(JSON.parse(body))) 29 | }) 30 | } 31 | 32 | function render (data) { 33 | var content = '' 34 | 35 | if (data) { 36 | content = yo` 37 |
38 |

Peer informationX

39 |
${asLinks(JSON.stringify(data, null, 2))}
40 |
41 | ` 42 | } 43 | 44 | const info = yo` 45 |
46 | ${content} 47 |
48 | ` 49 | 50 | return info 51 | } 52 | 53 | function hide () { 54 | lastId = null 55 | elem = yo.update(elem, render()) 56 | } 57 | 58 | function asLinks (text) { 59 | var results = [] 60 | var exp = /(https?:\/\/[-A-Z0-9+&@#/%?=~_|!:,.[\]]*[-A-Z0-9+&@#/%=~_|])/ig 61 | var match 62 | 63 | while ((match = exp.exec(text)) !== null) { 64 | results.push(yo`${text.slice(0, match.index)}`) 65 | results.push(yo`${match[1]}`) 66 | text = text.slice(match.index + match[1].length) 67 | } 68 | 69 | results.push(yo`${text}`) 70 | 71 | return results 72 | } 73 | } 74 | 75 | module.exports = build 76 | -------------------------------------------------------------------------------- /client/fill.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function setupFill (scale, maxColor) { 4 | const idColors = new Map() 5 | var paletteCounter = 0 6 | 7 | return fill 8 | 9 | function fill (d, i) { 10 | const name = d.id || d.data.id 11 | var color = idColors.get(name) 12 | if (!color) { 13 | color = scale(paletteCounter++) 14 | idColors.set(name, color) 15 | } 16 | 17 | if (paletteCounter >= maxColor) { 18 | paletteCounter = 0 19 | } 20 | 21 | return color 22 | } 23 | } 24 | 25 | module.exports = setupFill 26 | -------------------------------------------------------------------------------- /client/hash.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const d3 = require('d3') 4 | 5 | function create (radius, svg, color) { 6 | const arc = d3.arc() 7 | 8 | var innerRadius = 0 9 | var outerRadius = 0 10 | var path 11 | 12 | changeRadius(radius) 13 | 14 | return { plot, changeRadius, remove } 15 | 16 | function plot (main, point) { 17 | var startAngle 18 | var endAngle = 0 19 | 20 | main.selectAll('path') 21 | .each(function (p, i) { 22 | if (p.data.point < point.point) { 23 | startAngle = p.startAngle 24 | endAngle = p.endAngle 25 | } 26 | }) 27 | 28 | if (!path) { 29 | path = svg.selectAll('path') 30 | .data([{ 31 | startAngle, 32 | endAngle 33 | }]) 34 | .enter() 35 | .append('path') 36 | .attr('fill', color) 37 | .attr('d', arc) 38 | .attr('opacity', 0) 39 | 40 | path 41 | .transition() 42 | .duration(500) 43 | .attr('opacity', 1) 44 | } else { 45 | path 46 | .transition() 47 | .duration(1500) 48 | .attr('opacity', 1) 49 | .attrTween('d', arcTween(startAngle, endAngle)) 50 | } 51 | } 52 | 53 | function changeRadius (r) { 54 | computeVars(r) 55 | 56 | arc 57 | .innerRadius(innerRadius) 58 | .outerRadius(outerRadius) 59 | 60 | svg.selectAll('path') 61 | .transition('winresize') 62 | .attr('d', arc) 63 | } 64 | 65 | function computeVars (r) { 66 | innerRadius = r * 92 / 100 67 | outerRadius = r * 100 / 100 68 | } 69 | 70 | function remove () { 71 | if (path) { 72 | path.transition() 73 | .duration(500) 74 | .attr('opacity', 0) 75 | .remove() 76 | path = null 77 | } 78 | } 79 | 80 | function arcTween (startAngle, endAngle) { 81 | return function (d) { 82 | const interpolateEnd = d3.interpolate(d.endAngle, endAngle) 83 | const interpolateStart = d3.interpolate(d.startAngle, startAngle) 84 | return function (t) { 85 | d.endAngle = interpolateEnd(t) 86 | d.startAngle = interpolateStart(t) 87 | return arc(d) 88 | } 89 | } 90 | } 91 | } 92 | 93 | module.exports = create 94 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /* global WebSocket */ 3 | 4 | const d3 = require('d3') 5 | const sheetify = require('sheetify') 6 | const yo = require('yo-yo') 7 | const wr = require('winresize-event') 8 | const xhr = require('xhr') 9 | const d3sc = require('d3-scale-chromatic') 10 | const CompCol = require('complementary-colors') 11 | const maxColor = 7 12 | const maxInt = Math.pow(2, 32) - 1 13 | 14 | // TODO make the scale and the max number of color dynamic 15 | const scale = d3.scaleSequential(d3sc.interpolatePurples).domain([maxColor, 0]) 16 | const fill = require('./fill')(scale, maxColor) 17 | 18 | const compCol = new CompCol(scale(1)) 19 | const fillColor = asColor(compCol.triad()[2]) 20 | 21 | sheetify('normalize.css') 22 | const ringStyle = sheetify('./ring.css') 23 | const barStyle = sheetify('./bar.css') 24 | 25 | var width = 0 26 | var height = 0 27 | var radius = 0 28 | var innerRadius = 0 29 | var outerRadius = 0 30 | 31 | function computeSizes (dim) { 32 | width = Math.floor(dim.width * 98 / 100) 33 | height = Math.floor(dim.height * 98 / 100) 34 | radius = Math.min(width, height) / 2 35 | 36 | innerRadius = radius * 70 / 100 37 | outerRadius = radius * 90 / 100 38 | } 39 | 40 | computeSizes(wr.getWinSize()) 41 | 42 | const bar = yo` 43 |
44 | 45 | 46 | 47 |
48 | 49 |
50 |
51 | ` 52 | 53 | var lastPoint = null 54 | var pointTimer = null 55 | 56 | function changePoint (ev) { 57 | const key = ev.target.value 58 | 59 | if (key.length === 0) { 60 | hashDisplay.remove() 61 | return 62 | } 63 | 64 | clearTimeout(pointTimer) 65 | pointTimer = setTimeout(fetchPoint.bind(null, key), 100) 66 | } 67 | 68 | function fetchPoint (key) { 69 | pointTimer = null 70 | xhr.post('/hash', { body: key }, function (err, res, body) { 71 | if (err || res.statusCode !== 200) { 72 | hashDisplay.remove() 73 | return 74 | } 75 | 76 | lastPoint = JSON.parse(body) 77 | hashDisplay.plot(svg, lastPoint) 78 | }) 79 | } 80 | 81 | document.body.appendChild(bar) 82 | 83 | const parent = yo` 84 |
85 |
86 |
87 |
88 | ` 89 | 90 | document.body.appendChild(parent) 91 | 92 | const parentSvg = d3.select('div#wheel').append('svg') 93 | .attr('width', width) 94 | .attr('height', height) 95 | 96 | const svg3 = parentSvg.append('g') 97 | 98 | const svg = parentSvg 99 | .append('g') 100 | .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')') 101 | 102 | const svg2 = parentSvg 103 | .append('g') 104 | .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')') 105 | 106 | const arc = d3.arc() 107 | .innerRadius(innerRadius) 108 | .outerRadius(outerRadius) 109 | 110 | const { arcMouseOver, arcMouseLeave } = require('./mouseOver')(svg, fill) 111 | const hashDisplay = require('./hash')(radius, svg2, fillColor) 112 | const text = require('./text')(svg3, height, svg, arc, width, fill) 113 | const onclick = require('./details')(svg) 114 | const legend = require('./legend')(svg, onclick, arcMouseOver, arcMouseLeave, fill) 115 | 116 | wr.winResize.on(function (dim) { 117 | computeSizes(dim) 118 | 119 | d3.select('div#wheel').select('svg') 120 | .transition('winresize') 121 | .attr('width', width) 122 | .attr('height', height) 123 | 124 | svg.transition('winresize') 125 | .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')') 126 | 127 | svg2.transition('winresize') 128 | .attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')') 129 | 130 | arc 131 | .innerRadius(innerRadius) 132 | .outerRadius(outerRadius) 133 | 134 | svg.selectAll('path') 135 | .transition('winresize') 136 | .attr('d', arc) 137 | 138 | hashDisplay.changeRadius(radius) 139 | 140 | text.resize(width, height) 141 | }) 142 | 143 | const pie = d3.pie() 144 | .value(function (d) { 145 | return d.point 146 | }) 147 | .sort(null) 148 | 149 | const conn = new WebSocket(document.URL.replace('http', 'ws')) 150 | var path 151 | 152 | conn.onmessage = function (msg) { 153 | const data = JSON.parse(msg.data) 154 | 155 | if (data.ring) { 156 | // TODO add a transition 157 | if (path) { 158 | path.remove() 159 | } 160 | 161 | path = getPath(data.ring) 162 | text.clear() 163 | 164 | if (lastPoint) { 165 | setTimeout(function () { 166 | hashDisplay.plot(svg, lastPoint) 167 | }, 500) 168 | } 169 | 170 | legend(data.ring) 171 | } else if (data.trace) { 172 | data.trace.keys.forEach(function (pair) { 173 | text.add({ 174 | text: pair.key, 175 | point: pair.hash 176 | }) 177 | }) 178 | } 179 | } 180 | 181 | function getPath (data) { 182 | if (data.length > 0 && data[data.length - 1].point < maxInt) { 183 | data.push({ 184 | id: data[0].id, 185 | point: maxInt 186 | }) 187 | } 188 | data.unshift({ 189 | id: data[0].id, 190 | point: 0 191 | }) 192 | return svg.datum(data).selectAll('path') 193 | .data(pie) 194 | .enter() 195 | .append('path') 196 | .style('opacity', 1) 197 | .attr('fill', fill) 198 | .attr('d', arc) 199 | .on('mouseover', arcMouseOver) 200 | .on('mouseleave', arcMouseLeave) 201 | .on('click', onclick) 202 | } 203 | 204 | function asColor (obj) { 205 | return 'rgb(' + obj.r + ', ' + obj.g + ', ' + obj.b + ')' 206 | } 207 | -------------------------------------------------------------------------------- /client/info.css: -------------------------------------------------------------------------------- 1 | .info { 2 | position: absolute; 3 | text-align: left; 4 | padding: 30px; 5 | font: 1em sans-serif; 6 | background: rgba(241, 241, 241, 0.9); 7 | border: 1px; 8 | border-radius: 8px; 9 | margin: auto; 10 | position: absolute; 11 | top: 0; left: 0; bottom: 0; right: 0; 12 | width: 400px; 13 | height: 300px; 14 | } 15 | 16 | .close { 17 | float: right; 18 | } 19 | 20 | a { 21 | color: #3F007D; 22 | } 23 | -------------------------------------------------------------------------------- /client/legend.css: -------------------------------------------------------------------------------- 1 | :host { 2 | background-color: #F1F1F1; 3 | padding: 1em; 4 | position: absolute; 5 | right: 0px; 6 | top: 30%; 7 | border-radius: 0.5em 0 0 0.5em; 8 | } 9 | 10 | ul { 11 | padding-left: 0.5em; 12 | padding-right: 0.5em; 13 | } 14 | 15 | h2 { 16 | padding-left: 2.3em; 17 | } 18 | 19 | li { 20 | list-style-type: none; 21 | width: 100%; 22 | padding-top: 0.2em; 23 | padding-bottom: 0.2em; 24 | } 25 | 26 | .color { 27 | border-radius: 0.2em; 28 | width: 1em; 29 | height: 1em; 30 | float: left; 31 | display: block; 32 | border: black 1px; 33 | margin-right: 2em; 34 | } 35 | -------------------------------------------------------------------------------- /client/legend.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const yo = require('yo-yo') 4 | const sheetify = require('sheetify') 5 | const style = sheetify('./legend.css') 6 | 7 | function build (svg, onclick, onmouseover, onmouseleave, fill) { 8 | var elem = yo` 9 |
10 | ` 11 | 12 | document.body.appendChild(elem) 13 | 14 | return update 15 | 16 | function update (ring) { 17 | const peers = new Set() 18 | 19 | for (var i = 0; i < ring.length; i++) { 20 | peers.add(ring[i].id) 21 | } 22 | 23 | elem = yo.update(elem, render(peers)) 24 | } 25 | 26 | function render (peers) { 27 | const legend = yo` 28 |
29 |

Peers

30 | 31 |
32 | ` 33 | 34 | return legend 35 | } 36 | 37 | function asList (id) { 38 | const color = fill({ id }) 39 | return yo` 40 |
  • 41 | ${id} 42 |
  • 43 | ` 44 | 45 | function myonclick (ev) { 46 | ev.data = { id } 47 | onclick(ev) 48 | } 49 | 50 | function mymouseover (ev) { 51 | ev.data = { id } 52 | onmouseover(ev) 53 | } 54 | 55 | function mymouseleave (ev) { 56 | ev.data = { id } 57 | onmouseleave(ev) 58 | } 59 | } 60 | } 61 | 62 | module.exports = build 63 | -------------------------------------------------------------------------------- /client/mouseOver.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const d3 = require('d3') 4 | 5 | function setupMouseOver (svg, fill) { 6 | const div = d3.select('div#tooltip').style('opacity', 0) 7 | 8 | var lastId = '' 9 | 10 | return { arcMouseOver, arcMouseLeave } 11 | 12 | function arcMouseOver (ev) { 13 | const id = ev.data.id 14 | 15 | if (lastId !== id) { 16 | arcMouseLeave(ev) 17 | } 18 | 19 | svg.selectAll('path') 20 | .transition('mouseover') 21 | 22 | // to handle mouseOver without mouseLeave 23 | .attr('fill', fill) 24 | .style('opacity', 0.8) 25 | 26 | .filter(function (p) { 27 | return p.data.id !== id 28 | }) 29 | .attr('fill', '#333333') 30 | .style('opacity', 0.2) 31 | 32 | if (d3.event) { 33 | div.transition('mouseover') 34 | .duration(200) 35 | .style('opacity', 0.9) 36 | 37 | div.html(`id:${id}
    `) 38 | .style('left', (d3.event.pageX) + 'px') 39 | .style('top', (d3.event.pageY - 28) + 'px') 40 | } 41 | 42 | lastId = id 43 | } 44 | 45 | function arcMouseLeave (ev) { 46 | svg.selectAll('path') 47 | .transition('mouseover') 48 | .attr('fill', fill) 49 | .style('opacity', 1) 50 | 51 | div.transition('restore') 52 | .duration(200) 53 | .style('opacity', 0) 54 | 55 | lastId = '' 56 | } 57 | } 58 | 59 | module.exports = setupMouseOver 60 | -------------------------------------------------------------------------------- /client/ring.css: -------------------------------------------------------------------------------- 1 | 2 | svg { 3 | margin-left: auto; 4 | margin-right: auto; 5 | margin-top: auto; 6 | margin-bottom: auto; 7 | display: block; 8 | } 9 | 10 | #tooltip { 11 | position: absolute; 12 | text-align: center; 13 | padding: 20px; 14 | font: 12px sans-serif; 15 | background: #F1F1F1; 16 | border: 0px; 17 | border-radius: 8px; 18 | pointer-events: none; 19 | } 20 | 21 | .line { 22 | background-color: red; 23 | stroke-width: 3; 24 | } 25 | -------------------------------------------------------------------------------- /client/text.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const d3 = require('d3') 4 | 5 | function create (svg, height, main, arc, width, fill) { 6 | var lines = [] 7 | var lineHeight = 18 8 | 9 | return { add, resize, clear } 10 | 11 | function add (em) { 12 | lines.push(em) 13 | 14 | lines.reduceRight((acc, line) => { 15 | line.y = acc - lineHeight 16 | return acc - lineHeight 17 | }, height) 18 | 19 | var linkTo = null 20 | 21 | main.selectAll('path') 22 | .each(function (p, i) { 23 | if (!linkTo) { 24 | linkTo = p 25 | } 26 | if (p.data.point < em.point) { 27 | linkTo = p 28 | } 29 | }) 30 | 31 | var coord = arc.centroid(linkTo) 32 | 33 | em.id = linkTo.data.id 34 | 35 | // make them absolute 36 | em.arcCoord = { 37 | x: coord[0] + Math.floor(width / 2), 38 | y: coord[1] + Math.floor(height / 2) 39 | } 40 | 41 | var text = svg.selectAll('text') 42 | .data(lines) 43 | .enter() 44 | .append('text') 45 | 46 | text 47 | .attr('x', (d) => 0) 48 | .attr('y', (d) => d.y) 49 | .text((d) => d.text) 50 | .attr('font-family', 'sans-serif') 51 | .attr('font-size', lineHeight + 'px') 52 | .attr('fill', fill) 53 | .attr('opacity', 0) 54 | 55 | svg.selectAll('text') 56 | .filter((d) => { 57 | return d.y >= lineHeight * 2 58 | }) 59 | .transition() 60 | .duration(500) 61 | .attr('x', (d) => 0) 62 | .attr('y', (d) => d.y) 63 | .text((d) => d.text) 64 | .attr('opacity', 1) 65 | .each(function (d) { 66 | d.textLength = this.getComputedTextLength() 67 | }) 68 | 69 | svg.selectAll('text') 70 | .filter((d) => { 71 | return d.y < lineHeight * 2 72 | }) 73 | .transition() 74 | .duration(500) 75 | .attr('opacity', 0) 76 | .remove() 77 | 78 | svg.selectAll('path') 79 | .data(lines) 80 | .enter() 81 | .append('path') 82 | .attr('class', 'line') 83 | .attr('d', renderLine) 84 | .attr('fill', 'rgba(255,255,255,0)') 85 | .attr('opacity', 0) 86 | 87 | svg.selectAll('path') 88 | .filter((d) => { 89 | return d.y < lineHeight * 2 90 | }) 91 | .transition() 92 | .attr('opacity', 0) 93 | .remove() 94 | 95 | svg.selectAll('path') 96 | .filter((d) => { 97 | return d.y >= lineHeight * 2 98 | }) 99 | .transition() 100 | .attr('d', renderLine) 101 | .duration(500) 102 | .attr('stroke', fill) 103 | .attr('opacity', 0.5) 104 | 105 | lines = lines.filter((d) => { 106 | return d.y >= lineHeight * 2 107 | }) 108 | } 109 | 110 | function resize (w, h) { 111 | width = w 112 | height = h 113 | 114 | lines.reduceRight((acc, line) => { 115 | line.y = acc - lineHeight 116 | return acc - lineHeight 117 | }, height) 118 | 119 | svg.selectAll('text') 120 | .filter((d) => { 121 | return d.y >= lineHeight * 2 122 | }) 123 | .transition('winresize') 124 | .attr('x', (d) => 0) 125 | .attr('y', (d) => d.y) 126 | 127 | lines.forEach(function (em) { 128 | var linkTo 129 | 130 | main.selectAll('path') 131 | .each(function (p, i) { 132 | if (!linkTo) { 133 | linkTo = p 134 | } 135 | if (p.data.point < em.point) { 136 | linkTo = p 137 | } 138 | }) 139 | 140 | var coord = arc.centroid(linkTo) 141 | 142 | // make them absolute 143 | em.arcCoord = { 144 | x: coord[0] + Math.floor(width / 2), 145 | y: coord[1] + Math.floor(height / 2) 146 | } 147 | }) 148 | 149 | svg.selectAll('path') 150 | .transition('winresize') 151 | .attr('d', renderLine) 152 | } 153 | 154 | function clear () { 155 | lines = [] 156 | 157 | // TODO add a transition 158 | svg.selectAll('text') 159 | .remove() 160 | 161 | svg.selectAll('path') 162 | .remove() 163 | } 164 | 165 | function renderLine (d) { 166 | const source = { 167 | x: d.textLength + 10, 168 | y: Math.floor(d.y - (lineHeight / 2)) 169 | } 170 | const dest = d.arcCoord 171 | const mid = { 172 | x: Math.floor(dest.x / 2) - source.x, 173 | y: dest.y 174 | } 175 | const path = d3.path() 176 | 177 | path.moveTo(source.x, source.y) 178 | path.quadraticCurveTo(mid.x, mid.y, dest.x, dest.y) 179 | 180 | return path.toString() 181 | } 182 | } 183 | 184 | module.exports = create 185 | -------------------------------------------------------------------------------- /control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upringjs/upring-control/97f5436ac5f7a68e244d0d474a69e47d86b5dd9b/control.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const browserify = require('browserify') 4 | const bankai = require('bankai') 5 | const http = require('http') 6 | const path = require('path') 7 | const WebSocketServer = require('ws').Server 8 | const client = path.join(__dirname, 'client') 9 | const pump = require('pump') 10 | const fs = require('fs') 11 | 12 | function build (upring) { 13 | const assets = bankai() 14 | const css = assets.css() 15 | const js = assets.js(browserify, client) 16 | const html = assets.html() 17 | var _ready = false 18 | var isClient = false 19 | 20 | upring.on('up', function () { 21 | _ready = true 22 | try { 23 | upring.mymeta() 24 | } catch (err) { 25 | isClient = err && true 26 | } 27 | }) 28 | 29 | const server = http.createServer((req, res) => { 30 | if (req.url.indexOf('/peer/') === 0) { 31 | return ready(res) && peerDetails(req, res) 32 | } 33 | switch (req.url) { 34 | case '/': return html(req, res).pipe(res) 35 | case '/whoami': return ready(res) && whoami(req, res) 36 | case '/peers': return ready(res) && peers(req, res) 37 | case '/ring': return ready(res) && ring(req, res) 38 | case '/hash': return ready(res) && hash(req, res) 39 | case '/bundle.js': return js(req, res).pipe(res) 40 | case '/bundle.css': return css(req, res).pipe(res) 41 | case '/upring.png': 42 | res.setHeader('Content-Type', 'image/png') 43 | return pump(fs.createReadStream(path.join(__dirname, './upring.png')), res) 44 | default: 45 | res.statusCode = 404 46 | return res.end('404 not found') 47 | } 48 | }) 49 | 50 | const wss = new WebSocketServer({ server: server }) 51 | const connections = new Set() 52 | 53 | wss.on('connection', function connection (ws) { 54 | connections.add(ws) 55 | ws.on('close', function () { 56 | connections.delete(ws) 57 | }) 58 | ws.send(JSON.stringify(computeRing())) 59 | }) 60 | 61 | upring.on('peerUp', peerUp) 62 | upring.on('peerDown', sendRing) 63 | 64 | function sendRing () { 65 | const ring = computeRing() 66 | connections.forEach(function (conn) { 67 | conn.send(JSON.stringify(ring)) 68 | }) 69 | } 70 | 71 | function peerUp (peer) { 72 | sendRing() 73 | upring.peerConn(peer).request({ 74 | ns: 'monitoring', 75 | cmd: 'trace' 76 | }, function (err, res) { 77 | if (err) { 78 | upring.logger.warn(err) 79 | return 80 | } 81 | 82 | upring.logger.info({ peer }, 'trace set up') 83 | 84 | res.streams.trace.on('data', function (trace) { 85 | if (trace.keys.length === 0) { 86 | return 87 | } 88 | connections.forEach(function (conn) { 89 | conn.send(JSON.stringify({ trace })) 90 | }) 91 | }) 92 | }) 93 | } 94 | 95 | return server 96 | 97 | function ready (res) { 98 | if (!_ready) { 99 | res.statusCode = 503 100 | res.end('ring not ready yet') 101 | } 102 | return _ready 103 | } 104 | 105 | function whoami (req, res) { 106 | const id = upring.whoami() 107 | res.end(id) 108 | } 109 | 110 | function peers (req, res) { 111 | const peers = upring.peers(!isClient) 112 | res.end(JSON.stringify(peers, null, 2)) 113 | } 114 | 115 | function ring (req, res) { 116 | res.end(JSON.stringify(computeRing(), null, 2)) 117 | } 118 | 119 | function hash (req, res) { 120 | if (req.method !== 'POST') { 121 | res.statusCode = 404 122 | res.end() 123 | return 124 | } 125 | var chunks = '' 126 | req.setEncoding('utf8') 127 | req.on('data', function (d) { 128 | chunks += d 129 | }) 130 | req.on('end', function () { 131 | if (chunks.length === 0) { 132 | res.statusCode = 422 133 | res.end() 134 | return 135 | } 136 | res.end(JSON.stringify({ 137 | point: upring._hashring.hash(chunks) 138 | }, null, 2)) 139 | }) 140 | } 141 | 142 | function computeRing () { 143 | const ring = upring._hashring._entries.map(function (entry) { 144 | return { 145 | id: entry.peer.id, 146 | point: entry.point 147 | } 148 | }) 149 | 150 | return { ring } 151 | } 152 | 153 | function peerDetails (req, res) { 154 | const id = req.url.split('/peer/')[1] 155 | const conn = upring.peerConn({ id }) 156 | conn.request({ 157 | ns: 'monitoring', 158 | cmd: 'info' 159 | }, function (err, info) { 160 | if (err) { 161 | res.statusCode = 500 162 | res.end(err.message) 163 | return 164 | } 165 | res.end(JSON.stringify(info, null, 2)) 166 | }) 167 | } 168 | } 169 | 170 | module.exports = build 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upring-control", 3 | "version": "0.1.4", 4 | "description": "control panel for upring", 5 | "main": "index.js", 6 | "bin": { 7 | "upring-control": "./bin.js" 8 | }, 9 | "scripts": { 10 | "test": "standard | snazzy", 11 | "start": "./bin.js | pino" 12 | }, 13 | "precommit": "test", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/mcollina/upring-control.git" 17 | }, 18 | "keywords": [ 19 | "upring", 20 | "control", 21 | "ui", 22 | "hashring" 23 | ], 24 | "author": "Matteo Collina ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/mcollina/upring-control/issues" 28 | }, 29 | "homepage": "https://github.com/mcollina/upring-control#readme", 30 | "dependencies": { 31 | "bankai": "^3.3.1", 32 | "browserify": "^14.0.0", 33 | "bufferutil": "^1.2.1", 34 | "complementary-colors": "^1.0.2", 35 | "d3": "^4.3.0", 36 | "d3-scale-chromatic": "^1.1.0", 37 | "minimist": "^1.2.0", 38 | "normalize.css": "^5.0.0", 39 | "pino": "^4.0.0", 40 | "pino-http": "^2.0.0", 41 | "serve-static": "^1.11.1", 42 | "upring": "^0.14.0", 43 | "utf-8-validate": "^1.2.1", 44 | "winresize-event": "^0.2.1", 45 | "ws": "^1.1.1", 46 | "xhr": "^2.2.2", 47 | "yo-yo": "^1.3.1" 48 | }, 49 | "devDependencies": { 50 | "pre-commit": "^1.1.3", 51 | "snazzy": "^6.0.0", 52 | "standard": "^9.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /upring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upringjs/upring-control/97f5436ac5f7a68e244d0d474a69e47d86b5dd9b/upring.png --------------------------------------------------------------------------------