Implement multi-tier info boxes
These use HTML inside of SVG, this allows us to style the SVG's much easier, it also enables us to reference the dom nodes from within react.
This commit is contained in:
parent
1f3c6c2e41
commit
62aef3eec6
@ -7,7 +7,7 @@
|
|||||||
stroke-opacity: 1;
|
stroke-opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.health {
|
.health, .health_warn {
|
||||||
font-family: LibreFranklin;
|
font-family: LibreFranklin;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -16,6 +16,10 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.health_warn {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.stat {
|
.stat {
|
||||||
font-family: LibreFranklin;
|
font-family: LibreFranklin;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -25,6 +29,29 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node_statistics {
|
||||||
|
font-family: LibreFranklin;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node_statistics p {
|
||||||
|
margin: 0 0 0 0;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary, .secondary {
|
||||||
|
font-family: LibreFranklin;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-stretch: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<svg width='960' height='700'></svg>
|
<svg width='960' height='700'></svg>
|
||||||
<script src='https://d3js.org/d3.v4.min.js'></script>
|
<script src='https://d3js.org/d3.v4.min.js'></script>
|
||||||
@ -97,6 +124,211 @@ function rect(x, y, width, height, radius) {
|
|||||||
d3.json('services.json', function(error, graph) {
|
d3.json('services.json', function(error, graph) {
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
|
function createNode(elm) {
|
||||||
|
// Box where label will live
|
||||||
|
elm.append('path')
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('d', topRoundedRect('0', '0', 170, 47, 4))
|
||||||
|
.attr('stroke', '#343434')
|
||||||
|
.attr('stroke-width', '1px')
|
||||||
|
.attr('fill', '#464646')
|
||||||
|
|
||||||
|
// Hover-over text for a node's label.
|
||||||
|
var text = elm.append('g')
|
||||||
|
|
||||||
|
text.append('text')
|
||||||
|
.attr('class', 'info_text')
|
||||||
|
.attr('x', '12')
|
||||||
|
.attr('y', '30')
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('fill', '#fff')
|
||||||
|
.text(d => d.id)
|
||||||
|
|
||||||
|
text.append('circle')
|
||||||
|
.attr('class', 'alert')
|
||||||
|
.attr('cx', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 })
|
||||||
|
.attr('cy', '24')
|
||||||
|
.attr('stroke-width', '0px')
|
||||||
|
.attr('fill', (d) => d.id == 'Memcached' ? 'rgb(217, 77, 68)' : 'rgb(0,175,102)')
|
||||||
|
.attr('r', '9px')
|
||||||
|
|
||||||
|
// An icon or label that exists within the circle, inside the infobox
|
||||||
|
text.append('text')
|
||||||
|
.attr('class', 'health')
|
||||||
|
.attr('x', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 })
|
||||||
|
.attr('y', '29')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('fill', '#fff')
|
||||||
|
.text((d) => d.id == 'Memcached' ? '!' : '❤')
|
||||||
|
|
||||||
|
// Box where stats will live
|
||||||
|
var stats = elm.append('g');
|
||||||
|
|
||||||
|
stats.append('path')
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('d', (d) => d.id == 'Percona' ? rect('0', '-39', 170, 78, 2) : bottomRoundedRect('0', '-39', 170, 78, 4))
|
||||||
|
.attr('stroke', '#343434')
|
||||||
|
.attr('stroke-width', '1px')
|
||||||
|
.attr('fill', '#464646')
|
||||||
|
|
||||||
|
// An icon or label that exists within the circle, inside the infobox
|
||||||
|
stats.append('text')
|
||||||
|
.attr('class', 'cpu')
|
||||||
|
.attr('class', 'stat')
|
||||||
|
.attr('x', '12')
|
||||||
|
.attr('y', '65')
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('fill', 'rgba(255, 255, 255, 0.8)')
|
||||||
|
.text('CPU: 63%')
|
||||||
|
|
||||||
|
// An icon or label that exists within the circle, inside the infobox
|
||||||
|
stats.append('text')
|
||||||
|
.attr('class', 'memory')
|
||||||
|
.attr('class', 'stat')
|
||||||
|
.attr('x', '12')
|
||||||
|
.attr('y', '85')
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('fill', 'rgba(255, 255, 255, 0.8)')
|
||||||
|
.text('Memory: 50%')
|
||||||
|
|
||||||
|
// An icon or label that exists within the circle, inside the infobox
|
||||||
|
stats.append('text')
|
||||||
|
.attr('class', 'network')
|
||||||
|
.attr('class', 'stat')
|
||||||
|
.attr('x', '12')
|
||||||
|
.attr('y', '105')
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('fill', 'rgba(255, 255, 255, 0.8)')
|
||||||
|
.text('Network: 1.23kb/sec')
|
||||||
|
|
||||||
|
|
||||||
|
elm.call(d3.drag()
|
||||||
|
.on('start', dragstarted)
|
||||||
|
.on('drag', dragged)
|
||||||
|
.on('end', dragended));
|
||||||
|
|
||||||
|
elm.append('title')
|
||||||
|
.text(function(d) { return d.id; });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function createExtendedNode(elm) {
|
||||||
|
elm.append('path')
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('d', topRoundedRect('0', '0', 170, 47, 4))
|
||||||
|
.attr('stroke', '#343434')
|
||||||
|
.attr('stroke-width', '1px')
|
||||||
|
.attr('fill', '#464646')
|
||||||
|
|
||||||
|
// Hover-over text for a node's label.
|
||||||
|
var text = elm.append('g')
|
||||||
|
|
||||||
|
text.append('text')
|
||||||
|
.attr('class', 'info_text')
|
||||||
|
.attr('x', '12')
|
||||||
|
.attr('y', '30')
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('fill', '#fff')
|
||||||
|
.text(d => d.id)
|
||||||
|
|
||||||
|
// Box where stats will live
|
||||||
|
var stats = elm.append('g');
|
||||||
|
var primary = stats.append('g')
|
||||||
|
|
||||||
|
primary.append('path')
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('d', rect('0', '-39', 170, 78, 2))
|
||||||
|
.attr('stroke', '#343434')
|
||||||
|
.attr('stroke-width', '1px')
|
||||||
|
.attr('fill', '#464646')
|
||||||
|
|
||||||
|
primary.append('text')
|
||||||
|
.attr('class', 'primary')
|
||||||
|
.attr('x', '12')
|
||||||
|
.attr('y', '70')
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('fill', '#fff')
|
||||||
|
.text('Primary')
|
||||||
|
|
||||||
|
primary.append('circle')
|
||||||
|
.attr('class', 'alert')
|
||||||
|
.attr('cx', function () { return d3.select(this.parentNode).select('.primary').node().getBBox().width + 30 })
|
||||||
|
.attr('cy', '64')
|
||||||
|
.attr('stroke-width', '0px')
|
||||||
|
.attr('fill', 'rgb(229, 163, 57)')
|
||||||
|
.attr('r', '9px')
|
||||||
|
|
||||||
|
// An icon or label that exists within the circle, inside the infobox
|
||||||
|
primary.append('text')
|
||||||
|
.attr('class', 'health_warn')
|
||||||
|
.attr('x', function () { return d3.select(this.parentNode).select('.primary').node().getBBox().width + 30 })
|
||||||
|
.attr('y', '69')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('fill', '#fff')
|
||||||
|
.text('☇')
|
||||||
|
|
||||||
|
var secondary = stats.append('g');
|
||||||
|
|
||||||
|
secondary.append('path')
|
||||||
|
.attr('class', 'node')
|
||||||
|
.attr('d', bottomRoundedRect('0', '-113', 170, 114, 4))
|
||||||
|
.attr('stroke', '#343434')
|
||||||
|
.attr('stroke-width', '1px')
|
||||||
|
.attr('fill', '#464646')
|
||||||
|
|
||||||
|
secondary.append('text')
|
||||||
|
.attr('class', 'secondary')
|
||||||
|
.attr('x', '12')
|
||||||
|
.attr('y', '150')
|
||||||
|
.attr('text-anchor', 'start')
|
||||||
|
.attr('fill', '#fff')
|
||||||
|
.text('Secondary')
|
||||||
|
|
||||||
|
secondary.append('circle')
|
||||||
|
.attr('class', 'alert')
|
||||||
|
.attr('cx', function () { return d3.select(this.parentNode).select('.secondary').node().getBBox().width + 30 })
|
||||||
|
.attr('cy', '144')
|
||||||
|
.attr('stroke-width', '0px')
|
||||||
|
.attr('fill', 'rgb(0,175,102)')
|
||||||
|
.attr('r', '9px')
|
||||||
|
|
||||||
|
// An icon or label that exists within the circle, inside the infobox
|
||||||
|
secondary.append('text')
|
||||||
|
.attr('class', 'health')
|
||||||
|
.attr('x', function () { return d3.select(this.parentNode).select('.secondary').node().getBBox().width + 30 })
|
||||||
|
.attr('y', '149')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('fill', '#fff')
|
||||||
|
.text('❤')
|
||||||
|
|
||||||
|
var html = secondary
|
||||||
|
.append('switch')
|
||||||
|
.append('foreignObject')
|
||||||
|
.attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
|
||||||
|
.attr('x', 12)
|
||||||
|
.attr('y', 160)
|
||||||
|
.attr('width', 160)
|
||||||
|
.attr('height', 100)
|
||||||
|
// From here everything will be rendered with react using a ref.
|
||||||
|
// However for now these values are hard-coded.
|
||||||
|
.append('xhtml:div')
|
||||||
|
.attr('class', 'node_statistics')
|
||||||
|
|
||||||
|
// Remove with react + dyanmic data.
|
||||||
|
html.append('p')
|
||||||
|
.text('CPU: 48%')
|
||||||
|
html.append('p')
|
||||||
|
.text('Memory: 54%')
|
||||||
|
html.append('p')
|
||||||
|
.text('Network: 1.75kb/sec')
|
||||||
|
|
||||||
|
elm.call(d3.drag()
|
||||||
|
.on('start', dragstarted)
|
||||||
|
.on('drag', dragged)
|
||||||
|
.on('end', dragended));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Drawing the links between nodes
|
// Drawing the links between nodes
|
||||||
var link = svg.append('g')
|
var link = svg.append('g')
|
||||||
.attr('class', 'links')
|
.attr('class', 'links')
|
||||||
@ -112,90 +344,16 @@ d3.json('services.json', function(error, graph) {
|
|||||||
.append('g')
|
.append('g')
|
||||||
.attr('class', 'node_group');
|
.attr('class', 'node_group');
|
||||||
|
|
||||||
// Box where label will live
|
svg.selectAll('.node_group').each(function (d) {
|
||||||
node.append('path')
|
// Create different type of node for services with Primaries + Secondaries
|
||||||
.attr('class', 'node')
|
// We could extend this further to allow us to have as many nested services
|
||||||
.attr('d', topRoundedRect('0', '0', 170, 47, 4))
|
// as wanted.
|
||||||
.attr('stroke', '#343434')
|
if (d.id === 'Percona') {
|
||||||
.attr('stroke-width', '1px')
|
createExtendedNode(d3.select(this));
|
||||||
.attr('fill', '#464646')
|
} else {
|
||||||
|
createNode(d3.select(this));
|
||||||
// Hover-over text for a node's label.
|
}
|
||||||
var text = node.append('g')
|
});
|
||||||
|
|
||||||
text.append('text')
|
|
||||||
.attr('class', 'info_text')
|
|
||||||
.attr('x', '12')
|
|
||||||
.attr('y', '30')
|
|
||||||
.attr('text-anchor', 'start')
|
|
||||||
.attr('fill', '#fff')
|
|
||||||
.text(d => d.id)
|
|
||||||
|
|
||||||
text.append('circle')
|
|
||||||
.attr('class', 'alert')
|
|
||||||
.attr('cx', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 })
|
|
||||||
.attr('cy', '24')
|
|
||||||
.attr('stroke-width', '0px')
|
|
||||||
.attr('fill', (d) => d.id == 'Memcached' ? 'rgb(217, 77, 68)' : 'rgb(0,175,102)')
|
|
||||||
.attr('r', '9px')
|
|
||||||
|
|
||||||
// An icon or label that exists within the circle, inside the infobox
|
|
||||||
text.append('text')
|
|
||||||
.attr('class', 'health')
|
|
||||||
.attr('x', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 })
|
|
||||||
.attr('y', '29')
|
|
||||||
.attr('text-anchor', 'middle')
|
|
||||||
.attr('fill', '#fff')
|
|
||||||
.text((d) => d.id == 'Memcached' ? '!' : '❤')
|
|
||||||
|
|
||||||
// Box where stats will live
|
|
||||||
var stats = node.append('g');
|
|
||||||
|
|
||||||
stats.append('path')
|
|
||||||
.attr('class', 'node')
|
|
||||||
.attr('d', (d) => d.id == 'Percona' ? rect('0', '-39', 170, 78, 2) : bottomRoundedRect('0', '-39', 170, 78, 4))
|
|
||||||
.attr('stroke', '#343434')
|
|
||||||
.attr('stroke-width', '1px')
|
|
||||||
.attr('fill', '#464646')
|
|
||||||
|
|
||||||
// An icon or label that exists within the circle, inside the infobox
|
|
||||||
stats.append('text')
|
|
||||||
.attr('class', 'cpu')
|
|
||||||
.attr('class', 'stat')
|
|
||||||
.attr('x', '12')
|
|
||||||
.attr('y', '65')
|
|
||||||
.attr('text-anchor', 'start')
|
|
||||||
.attr('fill', 'rgba(255, 255, 255, 0.8)')
|
|
||||||
.text('CPU: 63%')
|
|
||||||
|
|
||||||
// An icon or label that exists within the circle, inside the infobox
|
|
||||||
stats.append('text')
|
|
||||||
.attr('class', 'memory')
|
|
||||||
.attr('class', 'stat')
|
|
||||||
.attr('x', '12')
|
|
||||||
.attr('y', '85')
|
|
||||||
.attr('text-anchor', 'start')
|
|
||||||
.attr('fill', 'rgba(255, 255, 255, 0.8)')
|
|
||||||
.text('Memory: 50%')
|
|
||||||
|
|
||||||
// An icon or label that exists within the circle, inside the infobox
|
|
||||||
stats.append('text')
|
|
||||||
.attr('class', 'network')
|
|
||||||
.attr('class', 'stat')
|
|
||||||
.attr('x', '12')
|
|
||||||
.attr('y', '105')
|
|
||||||
.attr('text-anchor', 'start')
|
|
||||||
.attr('fill', 'rgba(255, 255, 255, 0.8)')
|
|
||||||
.text('Network: 1.23kb/sec')
|
|
||||||
|
|
||||||
|
|
||||||
node.call(d3.drag()
|
|
||||||
.on('start', dragstarted)
|
|
||||||
.on('drag', dragged)
|
|
||||||
.on('end', dragended));
|
|
||||||
|
|
||||||
node.append('title')
|
|
||||||
.text(function(d) { return d.id; });
|
|
||||||
|
|
||||||
simulation
|
simulation
|
||||||
.nodes(graph.nodes)
|
.nodes(graph.nodes)
|
||||||
@ -209,6 +367,8 @@ d3.json('services.json', function(error, graph) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ticked() {
|
function ticked() {
|
||||||
|
// TODO: Remove these values and pull them out of the height of the boxes
|
||||||
|
// that the constraints belong to.
|
||||||
r=180
|
r=180
|
||||||
r2=130
|
r2=130
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user