├── .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 |
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 |
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 |
${Array.from(peers).map(asList)}
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
--------------------------------------------------------------------------------