blob: 29972fba1f54868c8e36be160e37dba49562fda3 [file] [log] [blame]
Matteo Scandolo9fe01af2016-02-09 16:01:49 -08001(function () {
2 'use strict';
3
4 const shapes = {
5 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'
6 }
7
Matteo Scandolo170d3be2016-02-11 08:58:04 -08008 var computeNodeId = 0;
9 var instanceId = 0;
10
Matteo Scandolo04564952016-02-24 11:22:48 -080011 angular.module('xos.diagnostic')
Matteo Scandolo26d17e12016-02-23 13:47:14 -080012 .service('NodeDrawer', function(d3, serviceTopologyConfig, RackHelper, lodash){
Matteo Scandolo51031482016-02-17 13:54:11 -080013
14 var _this = this;
15
Matteo Scandolo9fe01af2016-02-09 16:01:49 -080016 this.addNetworks = (nodes) => {
17 nodes.append('path')
18 .attr({
19 d: shapes.cloud,
20 transform: 'translate(-63, -52), scale(0.5)',
21 class: 'cloud'
22 });
23
24 nodes.append('text')
25 .attr({
Matteo Scandolo19acf7c2016-03-07 16:07:13 -080026 'text-anchor': 'middle',
27 y: -5,
28 x: 5,
Matteo Scandolo9fe01af2016-02-09 16:01:49 -080029 })
30 .text(d => d.name)
Matteo Scandolo45fba732016-02-22 14:53:44 -080031
Matteo Scandolo19acf7c2016-03-07 16:07:13 -080032 nodes.append('text')
33 .attr({
34 'text-anchor': 'middle',
35 y: 8,
36 x: 5,
37 class: 'small'
38 })
39 .text(d => d.subtitle)
40
Matteo Scandolo45fba732016-02-22 14:53:44 -080041 nodes.each(function(n){
42 let currentNode = d3.select(this);
43 // cicle trouch node to add Tags and Public IP
Matteo Scandolo79108192016-03-08 09:33:26 -080044 if(n.name === 'LAN-Side' && angular.isDefined(n.subscriberTag)){
Matteo Scandolo45fba732016-02-22 14:53:44 -080045 currentNode.append('text')
46 .attr({
47 'text-anchor': 'middle',
48 y: 40
49 })
50 .text(() => `C-Tag: ${n.subscriberTag.cTag}`);
51
52 currentNode.append('text')
53 .attr({
54 'text-anchor': 'middle',
55 y: 60
56 })
57 .text(() => `S-Tag: ${n.subscriberTag.sTag}`);
58 }
Matteo Scandolo012dddb2016-02-22 16:53:22 -080059
Matteo Scandolo79108192016-03-08 09:33:26 -080060 if(n.name === 'WAN-Side' && angular.isDefined(n.subscriberIP)){
Matteo Scandolo012dddb2016-02-22 16:53:22 -080061 currentNode.append('text')
62 .attr({
63 'text-anchor': 'middle',
64 y: 40
65 })
66 .text(() => `Public IP: ${n.subscriberIP}`);
67 }
Matteo Scandolo45fba732016-02-22 14:53:44 -080068 });
Matteo Scandolo9fe01af2016-02-09 16:01:49 -080069 }
70
71 this.addRack = (nodes) => {
Matteo Scandolo170d3be2016-02-11 08:58:04 -080072
Matteo Scandolo35d53c82016-02-16 14:44:51 -080073 // loop because of D3
74 // rack will be only one
75 nodes.each(d => {
76 let [w, h] = RackHelper.getRackSize(d.computeNodes);
Matteo Scandoload5b2282016-02-16 11:50:51 -080077
Matteo Scandolo26d17e12016-02-23 13:47:14 -080078 // TODO update instead of delete and redraw
79 nodes.select('g').remove();
80
Matteo Scandoload5b2282016-02-16 11:50:51 -080081 let rack = nodes
Matteo Scandolo35d53c82016-02-16 14:44:51 -080082 .append('g');
83
84 rack
85 .attr({
86 transform: `translate(0,0)`
87 })
88 .transition()
89 .duration(serviceTopologyConfig.duration)
90 .attr({
Matteo Scandolo26d17e12016-02-23 13:47:14 -080091 transform: () => `translate(${- (w / 2)}, ${- (h / 2)})`
Matteo Scandoload5b2282016-02-16 11:50:51 -080092 });
93
94 rack
95 .append('rect')
96 .attr({
Matteo Scandolo35d53c82016-02-16 14:44:51 -080097 width: 0,
98 height: 0
99 })
100 .transition()
101 .duration(serviceTopologyConfig.duration)
102 .attr({
Matteo Scandoload5b2282016-02-16 11:50:51 -0800103 width: w,
104 height: h
105 });
106
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800107 rack.append('text')
Matteo Scandoload5b2282016-02-16 11:50:51 -0800108 .attr({
109 'text-anchor': 'middle',
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800110 y: - 10,
111 x: w / 2,
112 opacity: 0
Matteo Scandoload5b2282016-02-16 11:50:51 -0800113 })
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800114 .text(d => d.name)
115 .transition()
116 .duration(serviceTopologyConfig.duration)
117 .attr({
118 opacity: 1
119 })
Matteo Scandoload5b2282016-02-16 11:50:51 -0800120
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800121 this.drawComputeNodes(rack, d.computeNodes);
122
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800123 });
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800124
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800125 };
126
127 this.drawComputeNodes = (container, nodes) => {
Matteo Scandoload5b2282016-02-16 11:50:51 -0800128
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800129 let elements = container.selectAll('.compute-nodes')
130 .data(nodes, d => {
131 if(!angular.isString(d.d3Id)){
132 d.d3Id = `compute-node-${++computeNodeId}`;
133 }
134 return d.d3Id;
135 });
136
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800137 let {width, height} = container.node().getBoundingClientRect();
138
139 var nodeContainer = elements.enter().append('g');
140
141 nodeContainer
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800142 .attr({
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800143 transform: `translate(${width / 2}, ${ height / 2})`,
Matteo Scandoload5b2282016-02-16 11:50:51 -0800144 class: 'compute-node',
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800145 })
146 .transition()
147 .duration(serviceTopologyConfig.duration)
148 .attr({
Matteo Scandoload5b2282016-02-16 11:50:51 -0800149 transform: (d) => `translate(${RackHelper.getComputeNodePosition(nodes, d.d3Id.replace('compute-node-', '') - 1)})`
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800150 });
151
152 nodeContainer.append('rect')
Matteo Scandoload5b2282016-02-16 11:50:51 -0800153 .attr({
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800154 width: 0,
155 height: 0
156 })
157 .transition()
158 .duration(serviceTopologyConfig.duration)
159 .attr({
Matteo Scandoload5b2282016-02-16 11:50:51 -0800160 width: d => RackHelper.getComputeNodeSize(d.instances)[0],
161 height: d => RackHelper.getComputeNodeSize(d.instances)[1],
162 });
163
164 nodeContainer.append('text')
165 .attr({
166 'text-anchor': 'start',
167 y: 15, //FIXME
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800168 x: 10, //FIXME
169 opacity: 0
Matteo Scandoload5b2282016-02-16 11:50:51 -0800170 })
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800171 .text(d => d.humanReadableName.split('.')[0])
172 .transition()
173 .duration(serviceTopologyConfig.duration)
174 .attr({
175 opacity: 1
176 })
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800177
178 // if there are Compute Nodes
179 if(nodeContainer.length > 0){
Matteo Scandolo51031482016-02-17 13:54:11 -0800180 // draw instances for each compute node
181 nodeContainer.each(function(a){
182 _this.drawInstances(d3.select(this), a.instances);
183 })
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800184 }
185
186 };
187
Matteo Scandolo7030ceb2016-02-16 13:29:26 -0800188 // NOTE Stripping unuseful names to shorten labels.
189 // This is not elegant
190 const formatInstanceName = (name) => {
191 return name
192 .replace('app_', '')
193 .replace('service_', '')
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800194 // .replace('ovs_', '')
Matteo Scandolo7030ceb2016-02-16 13:29:26 -0800195 .replace('mysite_', '')
196 .replace('_instance', '');
197 };
198
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800199 const getInstanceStatusColor = (instance) => {
200 function startWith(val, string){
201 return string.substring(0, val.length) === val;
202 }
203
204 if(startWith('0 - ', instance.backend_status)){
205 return 'provisioning';
206 }
207 if(startWith('1 - ', instance.backend_status)){
208 return 'good';
209 }
210 if(startWith('2 - ', instance.backend_status)){
211 return 'bad';
212 }
213 else {
214 return '';
215 }
216 };
217
Matteo Scandolof0d6e692016-02-24 11:14:01 -0800218 const drawContainer = (container, docker) => {
219
220 const containerBox = container.append('g')
221 .attr({
222 class: 'container',
223 transform: `translate(${serviceTopologyConfig.instance.margin}, 115)`
224 });
225
226 containerBox.append('rect')
227 .attr({
228 width: 250 - (serviceTopologyConfig.container.margin * 2),
229 height: serviceTopologyConfig.container.height,
230 });
231
232 containerBox.append('text')
233 .attr({
234 y: 20,
235 x: serviceTopologyConfig.instance.margin,
236 class: 'name'
237 })
238 .text(docker.name)
239
240 // add stats
241 const interestingMeters = ['memory', 'memory.usage', 'cpu_util'];
242
243 interestingMeters.forEach((m, i) => {
244 const meter = lodash.find(docker.stats, {meter: m});
245 // if there is no meter stats skip rendering
246 if(!angular.isDefined(meter)){
247 return;
248 }
249 containerBox.append('text')
250 .attr({
251 y: 40 + (i * 15),
252 x: serviceTopologyConfig.instance.margin,
253 opacity: 0
254 })
255 .text(`${meter.description}: ${Math.round(meter.value)} ${meter.unit}`)
256 .transition()
257 .duration(serviceTopologyConfig.duration)
258 .attr({
259 opacity: 1
260 });
261 });
262
263 // add port stats
264 const ports = ['eth0', 'eth1'];
265 const interestingPortMeters = [
266 {
267 meter: 'network.incoming.bytes.rate',
268 label: 'Incoming'
269 },
270 {
271 meter: 'network.outgoing.bytes.rate',
272 label: 'Outgoing'
273 }
274 ];
275
276 ports.forEach((p, j) => {
277
278 // if there are no port stats skip rendering
279 if(docker.port[p].length === 0){
280 return;
281 }
282
283 containerBox.append('text')
284 .attr({
285 y: 90,
286 x: serviceTopologyConfig.instance.margin + (120 * j),
287 class: 'name'
288 })
289 .text(`${docker.name}-${p}`)
290
291 interestingPortMeters.forEach((m, i) => {
292
293 const meter = lodash.find(docker.port[p], {meter: m.meter});
294 // if there is no meter stats skip rendering
295 if(!angular.isDefined(meter)){
296 return;
297 }
298 containerBox.append('text')
299 .attr({
300 y: 105 + (i * 15),
301 x: serviceTopologyConfig.instance.margin + (120 * j),
302 opacity: 0
303 })
304 .text(`${m.label}: ${Math.round(meter.value)} ${meter.unit}`)
305 .transition()
306 .duration(serviceTopologyConfig.duration)
307 .attr({
308 opacity: 1
309 });
310 });
311 });
312 }
313
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800314 const showInstanceStats = (container, instance) => {
315
316 // NOTE this should be dinamically positioned
317 // base on the number of element present
Matteo Scandolo963efd72016-03-10 08:59:27 -0800318
319 // fake the position
320 let translation = {
321 'mysite_vsg-1': '200, -120',
322 'mysite_vsg-2': '-300, 30',
323 'mysite_vsg-3': '-300, -250',
324 };
325
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800326 const statsContainer = container.append('g')
327 .attr({
Matteo Scandolo963efd72016-03-10 08:59:27 -0800328 transform: `translate(${translation[instance.humanReadableName]})`,
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800329 class: 'stats-container'
Matteo Scandolo963efd72016-03-10 08:59:27 -0800330 })
331 .on('click', function(d) {
332 // toggling visisbility
333 d.fade = !d.fade;
334 let opacity;
335 if(d.fade){
336 opacity = 0.1;
337 }
338 else{
339 opacity = 1;
340 }
341
342 d3.select(this)
343 .transition()
344 .duration(serviceTopologyConfig.duration)
345 .attr({
346 opacity: opacity
347 })
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800348 });
349
Matteo Scandolo963efd72016-03-10 08:59:27 -0800350 let lines = {
351 'mysite_vsg-1': {
Matteo Scandolof0d6e692016-02-24 11:14:01 -0800352 x1: -160,
353 y1: 120,
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800354 x2: 0,
355 y2: 50,
Matteo Scandolo963efd72016-03-10 08:59:27 -0800356 },
357 'mysite_vsg-2': {
358 x1: 250,
359 y1: 50,
360 x2: 300,
361 y2: -10
362 },
363 'mysite_vsg-3': {
364 x1: 250,
365 y1: 50,
366 x2: 300,
367 y2: 270
368 }
369 }
370
371 statsContainer.append('line')
372 .attr({
373 x1: d => lines[d.humanReadableName].x1,
374 y1: d => lines[d.humanReadableName].y1,
375 x2: d => lines[d.humanReadableName].x2,
376 y2: d => lines[d.humanReadableName].y2,
Matteo Scandolo6aa165f2016-02-23 14:03:03 -0800377 stroke: 'black',
378 opacity: 0
379 })
380 .transition()
381 .duration(serviceTopologyConfig.duration)
382 .attr({
383 opacity: 1
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800384 })
385
386 // NOTE rect should be dinamically sized base on the presence of a container
387 let statsHeight = 110;
Matteo Scandolof0d6e692016-02-24 11:14:01 -0800388 let statsWidth = 250;
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800389
390 if (instance.container){
391 statsHeight += serviceTopologyConfig.container.height + (serviceTopologyConfig.container.margin * 2)
392 }
393
Matteo Scandolo963efd72016-03-10 08:59:27 -0800394 const statsVignette = statsContainer.append('rect')
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800395 .attr({
396 width: statsWidth,
Matteo Scandolo6aa165f2016-02-23 14:03:03 -0800397 height: statsHeight,
398 opacity: 0
399 })
400 .transition()
401 .duration(serviceTopologyConfig.duration)
402 .attr({
403 opacity: 1
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800404 });
405
Matteo Scandolo963efd72016-03-10 08:59:27 -0800406
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800407 // add instance info
408 statsContainer.append('text')
409 .attr({
410 y: 15,
411 x: serviceTopologyConfig.instance.margin,
Matteo Scandolo6aa165f2016-02-23 14:03:03 -0800412 class: 'name',
413 opacity: 0
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800414 })
415 .text(instance.humanReadableName)
Matteo Scandolo6aa165f2016-02-23 14:03:03 -0800416 .transition()
417 .duration(serviceTopologyConfig.duration)
418 .attr({
419 opacity: 1
420 })
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800421
422 statsContainer.append('text')
423 .attr({
424 y: 30,
425 x: serviceTopologyConfig.instance.margin,
Matteo Scandolo6aa165f2016-02-23 14:03:03 -0800426 class: 'ip',
427 opacity: 0
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800428 })
429 .text(instance.ip)
Matteo Scandolo6aa165f2016-02-23 14:03:03 -0800430 .transition()
431 .duration(serviceTopologyConfig.duration)
432 .attr({
433 opacity: 1
434 })
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800435
436 // add stats
Matteo Scandolo963efd72016-03-10 08:59:27 -0800437 const interestingMeters = ['memory', 'memory.usage', 'cpu', 'cpu_util'];
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800438
439 interestingMeters.forEach((m, i) => {
440 const meter = lodash.find(instance.stats, {meter: m});
Matteo Scandolo963efd72016-03-10 08:59:27 -0800441
442 if(meter){
443
444 statsContainer.append('text')
445 .attr({
446 y: 55 + (i * 15),
447 x: serviceTopologyConfig.instance.margin,
448 opacity: 0
449 })
450 .text(`${meter.description}: ${Math.round(meter.value)} ${meter.unit}`)
451 .transition()
452 .duration(serviceTopologyConfig.duration)
453 .attr({
454 opacity: 1
455 });
456 }
457
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800458 });
459
460 if(instance.container){
461 // draw container
Matteo Scandolof0d6e692016-02-24 11:14:01 -0800462 drawContainer(statsContainer, instance.container);
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800463 }
464
465 };
466
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800467 this.drawInstances = (container, instances) => {
Matteo Scandolo51031482016-02-17 13:54:11 -0800468
Matteo Scandoloc49ff702016-02-17 15:11:33 -0800469 // TODO check for stats field in instance and draw popup
470
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800471 let {width, height} = container.node().getBoundingClientRect();
472
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800473 let elements = container.selectAll('.instances')
474 .data(instances, d => angular.isString(d.d3Id) ? d.d3Id : d.d3Id = `instance-${++instanceId}`)
475
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800476 var instanceContainer = elements.enter().append('g');
477
478 instanceContainer
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800479 .attr({
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800480 transform: `translate(${width / 2}, ${ height / 2})`,
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800481 class: d => `instance ${d.selected ? 'active' : ''} ${getInstanceStatusColor(d)}`,
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800482 })
483 .transition()
484 .duration(serviceTopologyConfig.duration)
485 .attr({
Matteo Scandolo51031482016-02-17 13:54:11 -0800486 transform: (d, i) => `translate(${RackHelper.getInstancePosition(i)})`
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800487 });
488
489 instanceContainer.append('rect')
Matteo Scandoload5b2282016-02-16 11:50:51 -0800490 .attr({
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800491 width: 0,
492 height: 0
493 })
494 .transition()
495 .duration(serviceTopologyConfig.duration)
496 .attr({
Matteo Scandoload5b2282016-02-16 11:50:51 -0800497 width: serviceTopologyConfig.instance.width,
498 height: serviceTopologyConfig.instance.height
499 });
500
501 instanceContainer.append('text')
502 .attr({
Matteo Scandolo7030ceb2016-02-16 13:29:26 -0800503 'text-anchor': 'middle',
504 y: 23, //FIXME
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800505 x: 40, //FIXME
506 opacity: 0
Matteo Scandoload5b2282016-02-16 11:50:51 -0800507 })
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800508 .text(d => formatInstanceName(d.humanReadableName))
Matteo Scandolo35d53c82016-02-16 14:44:51 -0800509 .transition()
510 .duration(serviceTopologyConfig.duration)
511 .attr({
512 opacity: 1
513 });
Matteo Scandolo7030ceb2016-02-16 13:29:26 -0800514
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800515 // if stats are attached and instance is active,
516 // draw stats
Matteo Scandolo6aa165f2016-02-23 14:03:03 -0800517 instanceContainer.each(function(instance, i){
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800518
519 const container = d3.select(this);
520
521 if(angular.isDefined(instance.stats) && instance.selected){
Matteo Scandolo6aa165f2016-02-23 14:03:03 -0800522 showInstanceStats(container, instance, i);
Matteo Scandolo26d17e12016-02-23 13:47:14 -0800523 }
524 });
525
Matteo Scandolo963efd72016-03-10 08:59:27 -0800526 // instanceContainer
527 // .on('click', function(d){
528 // console.log(`Draw vignette with stats for instance: ${d.name}`);
529 // });
Matteo Scandolo170d3be2016-02-11 08:58:04 -0800530 };
Matteo Scandolo9fe01af2016-02-09 16:01:49 -0800531
532 this.addPhisical = (nodes) => {
Matteo Scandolo3a176a22016-03-07 16:42:03 -0800533
534 nodes.select('rect').remove();
535 nodes.select('text').remove();
536
Matteo Scandolo9fe01af2016-02-09 16:01:49 -0800537 nodes.append('rect')
538 .attr(serviceTopologyConfig.square);
539
540 nodes.append('text')
541 .attr({
542 'text-anchor': 'middle',
543 y: serviceTopologyConfig.square.y - 10
544 })
Matteo Scandolod4ea8772016-03-01 15:20:29 -0800545 .text(d => {
546 return d.name || d.humanReadableName
547 });
Matteo Scandolo9fe01af2016-02-09 16:01:49 -0800548 }
549
550 this.addDevice = (nodes) => {
551 nodes.append('circle')
552 .attr(serviceTopologyConfig.circle);
553
554 nodes.append('text')
555 .attr({
556 'text-anchor': 'end',
557 x: - serviceTopologyConfig.circle.r - 10,
558 y: serviceTopologyConfig.circle.r / 2
559 })
Matteo Scandolo50eeec62016-02-23 10:04:36 -0800560 .text(d => d.name || d.mac);
Matteo Scandolo9fe01af2016-02-09 16:01:49 -0800561 }
562 });
563})();