| (function () { |
| 'use strict'; |
| |
| const shapes = { |
| cloud: ' M 79.72 49.60 C 86.00 37.29 98.57 29.01 111.96 26.42 C 124.27 24.11 137.53 26.15 148.18 32.90 C 158.08 38.78 165.39 48.87 167.65 60.20 C 176.20 57.90 185.14 56.01 194.00 57.73 C 206.08 59.59 217.92 66.01 224.37 76.66 C 227.51 81.54 228.85 87.33 229.23 93.06 C 237.59 93.33 246.22 95.10 253.04 100.19 C 256.69 103.13 259.87 107.67 258.91 112.59 C 257.95 118.43 252.78 122.38 247.78 124.82 C 235.27 130.43 220.23 130.09 207.98 123.93 C 199.33 127.88 189.76 129.43 180.30 128.57 C 173.70 139.92 161.70 147.65 148.86 149.93 C 133.10 153.26 116.06 148.15 104.42 137.08 C 92.98 143.04 78.96 143.87 66.97 139.04 C 57.75 135.41 49.70 128.00 46.60 118.43 C 43.87 109.95 45.81 100.29 51.30 93.32 C 57.38 85.18 67.10 80.44 76.99 78.89 C 74.38 69.20 74.87 58.52 79.72 49.60 Z' |
| } |
| |
| var computeNodeId = 0; |
| var instanceId = 0; |
| |
| angular.module('xos.diagnostic') |
| .service('NodeDrawer', function(d3, serviceTopologyConfig, RackHelper, lodash){ |
| |
| var _this = this; |
| |
| this.addNetworks = (nodes) => { |
| nodes.append('path') |
| .attr({ |
| d: shapes.cloud, |
| transform: 'translate(-63, -52), scale(0.5)', |
| class: 'cloud' |
| }); |
| |
| nodes.append('text') |
| .attr({ |
| 'text-anchor': 'middle', |
| y: -5, |
| x: 5, |
| }) |
| .text(d => d.name) |
| |
| nodes.append('text') |
| .attr({ |
| 'text-anchor': 'middle', |
| y: 8, |
| x: 5, |
| class: 'small' |
| }) |
| .text(d => d.subtitle) |
| |
| nodes.each(function(n){ |
| let currentNode = d3.select(this); |
| // cicle trouch node to add Tags and Public IP |
| if(n.name === 'LAN-Side' && angular.isDefined(n.subscriberTag)){ |
| currentNode.append('text') |
| .attr({ |
| 'text-anchor': 'middle', |
| y: 40 |
| }) |
| .text(() => `C-Tag: ${n.subscriberTag.cTag}`); |
| |
| currentNode.append('text') |
| .attr({ |
| 'text-anchor': 'middle', |
| y: 60 |
| }) |
| .text(() => `S-Tag: ${n.subscriberTag.sTag}`); |
| } |
| |
| if(n.name === 'WAN-Side' && angular.isDefined(n.subscriberIP)){ |
| currentNode.append('text') |
| .attr({ |
| 'text-anchor': 'middle', |
| y: 40 |
| }) |
| .text(() => `Public IP: ${n.subscriberIP}`); |
| } |
| }); |
| } |
| |
| this.addRack = (nodes) => { |
| |
| // loop because of D3 |
| // rack will be only one |
| nodes.each(d => { |
| let [w, h] = RackHelper.getRackSize(d.computeNodes); |
| |
| // TODO update instead of delete and redraw |
| nodes.select('g').remove(); |
| |
| let rack = nodes |
| .append('g'); |
| |
| rack |
| .attr({ |
| transform: `translate(0,0)` |
| }) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| transform: () => `translate(${- (w / 2)}, ${- (h / 2)})` |
| }); |
| |
| rack |
| .append('rect') |
| .attr({ |
| width: 0, |
| height: 0 |
| }) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| width: w, |
| height: h |
| }); |
| |
| rack.append('text') |
| .attr({ |
| 'text-anchor': 'middle', |
| y: - 10, |
| x: w / 2, |
| opacity: 0 |
| }) |
| .text(d => d.name) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }) |
| |
| this.drawComputeNodes(rack, d.computeNodes); |
| |
| }); |
| |
| }; |
| |
| this.drawComputeNodes = (container, nodes) => { |
| |
| let elements = container.selectAll('.compute-nodes') |
| .data(nodes, d => { |
| if(!angular.isString(d.d3Id)){ |
| d.d3Id = `compute-node-${++computeNodeId}`; |
| } |
| return d.d3Id; |
| }); |
| |
| let {width, height} = container.node().getBoundingClientRect(); |
| |
| var nodeContainer = elements.enter().append('g'); |
| |
| nodeContainer |
| .attr({ |
| transform: `translate(${width / 2}, ${ height / 2})`, |
| class: 'compute-node', |
| }) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| transform: (d) => `translate(${RackHelper.getComputeNodePosition(nodes, d.d3Id.replace('compute-node-', '') - 1)})` |
| }); |
| |
| nodeContainer.append('rect') |
| .attr({ |
| width: 0, |
| height: 0 |
| }) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| width: d => RackHelper.getComputeNodeSize(d.instances)[0], |
| height: d => RackHelper.getComputeNodeSize(d.instances)[1], |
| }); |
| |
| nodeContainer.append('text') |
| .attr({ |
| 'text-anchor': 'start', |
| y: 15, //FIXME |
| x: 10, //FIXME |
| opacity: 0 |
| }) |
| .text(d => d.humanReadableName.split('.')[0]) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }) |
| |
| // if there are Compute Nodes |
| if(nodeContainer.length > 0){ |
| // draw instances for each compute node |
| nodeContainer.each(function(a){ |
| _this.drawInstances(d3.select(this), a.instances); |
| }) |
| } |
| |
| }; |
| |
| // NOTE Stripping unuseful names to shorten labels. |
| // This is not elegant |
| const formatInstanceName = (name) => { |
| return name |
| .replace('app_', '') |
| .replace('service_', '') |
| // .replace('ovs_', '') |
| .replace('mysite_', '') |
| .replace('_instance', ''); |
| }; |
| |
| const getInstanceStatusColor = (instance) => { |
| function startWith(val, string){ |
| return string.substring(0, val.length) === val; |
| } |
| |
| if(startWith('0 - ', instance.backend_status)){ |
| return 'provisioning'; |
| } |
| if(startWith('1 - ', instance.backend_status)){ |
| return 'good'; |
| } |
| if(startWith('2 - ', instance.backend_status)){ |
| return 'bad'; |
| } |
| else { |
| return ''; |
| } |
| }; |
| |
| const drawContainer = (container, docker) => { |
| |
| const containerBox = container.append('g') |
| .attr({ |
| class: 'container', |
| transform: `translate(${serviceTopologyConfig.instance.margin}, 115)` |
| }); |
| |
| containerBox.append('rect') |
| .attr({ |
| width: 250 - (serviceTopologyConfig.container.margin * 2), |
| height: serviceTopologyConfig.container.height, |
| }); |
| |
| containerBox.append('text') |
| .attr({ |
| y: 20, |
| x: serviceTopologyConfig.instance.margin, |
| class: 'name' |
| }) |
| .text(docker.name) |
| |
| // add stats |
| const interestingMeters = ['memory', 'memory.usage', 'cpu_util']; |
| |
| interestingMeters.forEach((m, i) => { |
| const meter = lodash.find(docker.stats, {meter: m}); |
| // if there is no meter stats skip rendering |
| if(!angular.isDefined(meter)){ |
| return; |
| } |
| containerBox.append('text') |
| .attr({ |
| y: 40 + (i * 15), |
| x: serviceTopologyConfig.instance.margin, |
| opacity: 0 |
| }) |
| .text(`${meter.description}: ${Math.round(meter.value)} ${meter.unit}`) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }); |
| }); |
| |
| // add port stats |
| const ports = ['eth0', 'eth1']; |
| const interestingPortMeters = [ |
| { |
| meter: 'network.incoming.bytes.rate', |
| label: 'Incoming' |
| }, |
| { |
| meter: 'network.outgoing.bytes.rate', |
| label: 'Outgoing' |
| } |
| ]; |
| |
| ports.forEach((p, j) => { |
| |
| // if there are no port stats skip rendering |
| if(docker.port[p].length === 0){ |
| return; |
| } |
| |
| containerBox.append('text') |
| .attr({ |
| y: 90, |
| x: serviceTopologyConfig.instance.margin + (120 * j), |
| class: 'name' |
| }) |
| .text(`${docker.name}-${p}`) |
| |
| interestingPortMeters.forEach((m, i) => { |
| |
| const meter = lodash.find(docker.port[p], {meter: m.meter}); |
| // if there is no meter stats skip rendering |
| if(!angular.isDefined(meter)){ |
| return; |
| } |
| containerBox.append('text') |
| .attr({ |
| y: 105 + (i * 15), |
| x: serviceTopologyConfig.instance.margin + (120 * j), |
| opacity: 0 |
| }) |
| .text(`${m.label}: ${Math.round(meter.value)} ${meter.unit}`) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }); |
| }); |
| }); |
| } |
| |
| const showInstanceStats = (container, instance) => { |
| |
| // NOTE this should be dinamically positioned |
| // base on the number of element present |
| |
| // fake the position |
| let translation = { |
| 'mysite_vsg-1': '200, -120', |
| 'mysite_vsg-2': '-300, 30', |
| 'mysite_vsg-3': '-300, -250', |
| }; |
| |
| const statsContainer = container.append('g') |
| .attr({ |
| transform: `translate(${translation[instance.humanReadableName] || translation['mysite_vsg-1']})`, |
| class: 'stats-container' |
| }) |
| .on('click', function(d) { |
| // toggling visisbility |
| d.fade = !d.fade; |
| let opacity; |
| if(d.fade){ |
| opacity = 0.1; |
| } |
| else{ |
| opacity = 1; |
| } |
| |
| d3.select(this) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: opacity |
| }) |
| }); |
| |
| let lines = { |
| 'mysite_vsg-1': { |
| x1: -160, |
| y1: 120, |
| x2: 0, |
| y2: 50, |
| }, |
| 'mysite_vsg-2': { |
| x1: 250, |
| y1: 50, |
| x2: 300, |
| y2: -10 |
| }, |
| 'mysite_vsg-3': { |
| x1: 250, |
| y1: 50, |
| x2: 300, |
| y2: 270 |
| } |
| } |
| |
| statsContainer.append('line') |
| .attr({ |
| x1: d => lines[d.humanReadableName].x1 || lines['mysite_vsg-1'], |
| y1: d => lines[d.humanReadableName].y1 || lines['mysite_vsg-1'], |
| x2: d => lines[d.humanReadableName].x2 || lines['mysite_vsg-1'], |
| y2: d => lines[d.humanReadableName].y2 || lines['mysite_vsg-1'], |
| stroke: 'black', |
| opacity: 0 |
| }) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }) |
| |
| // NOTE rect should be dinamically sized base on the presence of a container |
| let statsHeight = 110; |
| let statsWidth = 250; |
| |
| if (instance.container){ |
| statsHeight += serviceTopologyConfig.container.height + (serviceTopologyConfig.container.margin * 2) |
| } |
| |
| const statsVignette = statsContainer.append('rect') |
| .attr({ |
| width: statsWidth, |
| height: statsHeight, |
| opacity: 0 |
| }) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }); |
| |
| |
| // add instance info |
| statsContainer.append('text') |
| .attr({ |
| y: 15, |
| x: serviceTopologyConfig.instance.margin, |
| class: 'name', |
| opacity: 0 |
| }) |
| .text(instance.humanReadableName) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }) |
| |
| statsContainer.append('text') |
| .attr({ |
| y: 30, |
| x: serviceTopologyConfig.instance.margin, |
| class: 'ip', |
| opacity: 0 |
| }) |
| .text(instance.ip) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }) |
| |
| // add stats |
| const interestingMeters = ['memory', 'memory.usage', 'cpu', 'cpu_util']; |
| |
| interestingMeters.forEach((m, i) => { |
| const meter = lodash.find(instance.stats, {meter: m}); |
| |
| if(meter){ |
| |
| statsContainer.append('text') |
| .attr({ |
| y: 55 + (i * 15), |
| x: serviceTopologyConfig.instance.margin, |
| opacity: 0 |
| }) |
| .text(`${meter.description}: ${Math.round(meter.value)} ${meter.unit}`) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }); |
| } |
| |
| }); |
| |
| if(instance.container){ |
| // draw container |
| drawContainer(statsContainer, instance.container); |
| } |
| |
| }; |
| |
| this.drawInstances = (container, instances) => { |
| |
| // TODO check for stats field in instance and draw popup |
| |
| let {width, height} = container.node().getBoundingClientRect(); |
| |
| let elements = container.selectAll('.instances') |
| .data(instances, d => angular.isString(d.d3Id) ? d.d3Id : d.d3Id = `instance-${++instanceId}`) |
| |
| var instanceContainer = elements.enter().append('g'); |
| |
| instanceContainer |
| .attr({ |
| transform: `translate(${width / 2}, ${ height / 2})`, |
| class: d => `instance ${d.selected ? 'active' : ''} ${getInstanceStatusColor(d)}`, |
| }) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| transform: (d, i) => `translate(${RackHelper.getInstancePosition(i)})` |
| }); |
| |
| instanceContainer.append('rect') |
| .attr({ |
| width: 0, |
| height: 0 |
| }) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| width: serviceTopologyConfig.instance.width, |
| height: serviceTopologyConfig.instance.height |
| }); |
| |
| instanceContainer.append('text') |
| .attr({ |
| 'text-anchor': 'middle', |
| y: 23, //FIXME |
| x: 40, //FIXME |
| opacity: 0 |
| }) |
| .text(d => formatInstanceName(d.humanReadableName)) |
| .transition() |
| .duration(serviceTopologyConfig.duration) |
| .attr({ |
| opacity: 1 |
| }); |
| |
| // if stats are attached and instance is active, |
| // draw stats |
| instanceContainer.each(function(instance, i){ |
| |
| const container = d3.select(this); |
| |
| if(angular.isDefined(instance.stats) && instance.selected){ |
| showInstanceStats(container, instance, i); |
| } |
| }); |
| |
| // instanceContainer |
| // .on('click', function(d){ |
| // console.log(`Draw vignette with stats for instance: ${d.name}`); |
| // }); |
| }; |
| |
| this.addPhisical = (nodes) => { |
| |
| nodes.select('rect').remove(); |
| nodes.select('text').remove(); |
| |
| nodes.append('rect') |
| .attr(serviceTopologyConfig.square); |
| |
| nodes.append('text') |
| .attr({ |
| 'text-anchor': 'middle', |
| y: serviceTopologyConfig.square.y - 10 |
| }) |
| .text(d => { |
| return d.name || d.humanReadableName |
| }); |
| } |
| |
| this.addDevice = (nodes) => { |
| nodes.append('circle') |
| .attr(serviceTopologyConfig.circle); |
| |
| nodes.append('text') |
| .attr({ |
| 'text-anchor': 'end', |
| x: - serviceTopologyConfig.circle.r - 10, |
| y: serviceTopologyConfig.circle.r / 2 |
| }) |
| .text(d => d.name || d.mac); |
| } |
| }); |
| })(); |