First pass for ovsdb-ui
[netvirt.git] / ovsdb-ui / module / src / main / resources / ovsdb / LogicalGraph.js
1 define(['app/ovsdb/lib/d3.min', 'app/ovsdb/OvsCore', 'underscore'], function(d3, OvsCore, _) {
2   'use strict';
3
4   var root = null,
5     canvasWidth = -1,
6     canvasHeight = -1,
7     bbox = { x:0, y:15, width: 0, height: 0},
8     // config
9     nodeWidth = 15,
10     nodeHeight = -1,
11     defaultRouterWidth = 66,
12     defaultRouterHeight = 66,
13     networkMargin = { width: 120, height: 15},
14     routerMargin = { width: 120, height: 40},
15     vmMargin = { width: 90, height: 30},
16     defaultVmsWidth = 48,
17     defaultVmsHeight = 48,
18     ipNetworkTextMaxLength = 60,
19     networkOffset = 15,
20     linkHeight = 5,
21     // datas
22     networkData = [],
23     routerData = [],
24     vmData = [],
25     linkData = [],
26     tmpNetHolder = {},
27     // d3 layer over datas
28     d3Node = null,
29     d3Link = null,
30     d3Vm = null,
31     d3Router = null,
32     randomize = OvsCore.Util.Math.Random(42);
33
34   function LogicalGraph(id, width , height) {
35     canvasWidth = width;
36     canvasHeight = height;
37
38     nodeHeight = height - 15;
39
40     var tmp = d3.select(id).append("svg")
41     .attr('width', width)
42     .attr('height', height)
43     .append("svg:g")
44     .attr('class', 'layer_0');
45
46     tmp.append('svg:rect')
47     .attr('width', width)
48     .attr('height', height)
49     .attr('fill', 'white');
50
51     root = tmp.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
52     .append("g");
53     tmp.on("dblclick.zoom", null);
54     addDefs();
55   }
56
57   // Define reusable svg item like box-shadow
58   function addDefs() {
59     // box-shadow
60     var defs = d3.select('svg').insert('svg:defs', ':first-child');
61     var filter = defs.append('svg:filter').attr('id', 'boxShadow').attr('x', '0').attr('y', '0').attr('width', '200%').attr('height', '200%');
62     filter.append('feOffset').attr('in', 'SourceAlpha').attr('result', 'offOut').attr('dx', 0).attr('dy', 0);
63     filter.append('feGaussianBlur').attr('stdDeviation', '5').attr('in','offOut').attr('result', 'blurOut');
64     filter.append('feOffset').attr('in', 'SourceGraphic').attr('in2', 'blurOut').attr('mode', 'normal');
65   }
66
67   function zoom() {
68     root.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
69   }
70
71   Object.defineProperties(LogicalGraph.prototype, {
72     networks: {
73       set: function(value) {
74         networkData = value;
75         value.forEach(function(net) {
76           routerData = routerData.concat(net.routers);
77         });
78         value.forEach(function(net) {
79           vmData = vmData.concat(net.instances);
80         });
81       }
82     }
83   });
84
85   LogicalGraph.prototype.start = function() {
86     setTopologyPosition.call(this, networkData);
87     addLinksToDom.call(this, linkData);
88     addNetWorkRouterVmsToDom.call(this, networkData, routerData, vmData);
89     update.call(this);
90   };
91
92   LogicalGraph.prototype.freeDOM = function() {
93     d3Node.remove();
94     d3Link.remove();
95     d3Vm.remove();
96     d3Router.remove();
97
98     networkData = [];
99     routerData = [];
100     vmData = [];
101     linkData = [];
102     bbox = { x:0, y:15, width: 0, height: 0};
103   };
104
105   function addLinksToDom(linksData) {
106       d3Link = root.selectAll('.llink')
107       .data(linksData).enter().append('svg:g');
108
109       d3Link.append('rect')
110       .attr('width', function(d) {
111         return d.target.x - d.source.x;
112       })
113       .attr('height', linkHeight)
114       .style('fill', function(d) {
115         return d.color;
116       });
117
118       d3Link.append('text')
119       .attr('x', 40)
120       .attr('y', -3)
121       .text(function(d) {return d.text;});
122   }
123
124   function addNetWorkRouterVmsToDom(networks, routers, vms) {
125     var ctx = this,
126       timer = null;
127
128     function getAbsPos() {
129       var elem = d3.select(this)[0][0],
130         elemAbsBBox = elem.getBoundingClientRect(),
131         parentAbsBox = d3.select('#l_graph').node().getBoundingClientRect(),
132         left = elemAbsBBox.left - parentAbsBox.left + (elemAbsBBox.right - elemAbsBBox.left),
133         top = elemAbsBBox.top  - parentAbsBox.top;
134
135       if (top < 0 ) {
136         top = 10;
137       }
138       var event = {
139          left : left,
140          top : top
141        };
142        return event;
143     }
144     d3Node = root.selectAll('.network')
145       .data(networks).enter()
146       .append('svg:g');
147     d3Router = root.selectAll('.routers')
148       .data(routers).enter()
149       .append('svg:g');
150     d3Vm = root.selectAll('.vm')
151       .data(vms).enter()
152       .append('svg:g');
153
154     // append coresponding form
155     d3Node.append('svg:rect')
156       .attr('width', nodeWidth)
157       .attr('height', nodeHeight)
158       .attr('rx', 10)
159       .attr('ry', 10)
160       .style('fill', function(d) {
161         return d.color;
162       }).on('click', function(d) {
163         if (d3.event.defaultPrevented) return;
164         timer = setTimeout(function() {
165           var e = getAbsPos.call(this);
166           ctx.onClick(e, d);
167         }.bind(this), 150);
168       }).on('dblclick', function(d) {
169         clearTimeout(timer);
170         ctx.dblClick(d);
171       });
172
173     // append the network name text
174     d3Node.append('text')
175       .attr('x', nodeWidth / 2 )
176       .attr('y', nodeHeight /2 )
177       .style('text-anchor', 'middle')
178       .style('writing-mode', 'tb')
179       .style('font-size', '12px')
180       .style('glyph-orientation-vertical', '0')
181       .text(function(d) { return d.name; });
182
183     // text info for the network ip
184     d3Node.append('text')
185       .attr('x', nodeWidth + 10)
186       .attr('y', nodeHeight - 15)
187       .attr('transform',
188         OvsCore.Util.String.Format('translate({0} {1}) rotate(-90) translate(-{0} -{1})', nodeWidth + 10, nodeHeight - 15))
189       .attr('class', 'linfolabel')
190       .text(function(d) {return d.ip;});
191
192     // vm
193     d3Vm.append('svg:image')
194       .attr('width', defaultVmsWidth)
195       .attr('height', defaultVmsHeight)
196       .attr('filter', 'url(#boxShadow)')
197       .attr('xlink:href', function(d) {
198         return d.type === 'network:dhcp' ?
199           'src/app/ovsdb/assets/dhcp.png' : 'src/app/ovsdb/assets/vm.png';
200       })
201       .on('click', function(d) {
202         if (d3.event.defaultPrevented) return;
203         timer = setTimeout(function() {
204           var e = getAbsPos.call(this);
205           ctx.onClick(e, d);
206         }.bind(this), 150);
207       }).on('dblclick', function(d) {
208         clearTimeout(timer);
209         ctx.dblClick(d);
210       });
211
212     // router
213     d3Router.append('svg:image')
214       .attr('width', defaultRouterWidth)
215       .attr('height', defaultRouterHeight)
216       .attr('xlink:href', 'src/app/ovsdb/assets/router.png')
217       .on('click', function(d) {
218         if (d3.event.defaultPrevented) return;
219         timer = setTimeout(function() {
220           var e = getAbsPos.call(this);
221           ctx.onClick(e, d);
222         }.bind(this), 150);
223       }).on('dblclick', function(d) {
224         clearTimeout(timer);
225         ctx.dblClick(d);
226       });
227
228     // router name label
229     d3Router.append('text')
230       .attr('x', defaultRouterWidth * 0.5)
231       .attr('y', defaultRouterHeight + 15)
232       .attr('text-anchor', 'middle')
233       .attr('class', 'linfolabel')
234       .text(function(d) { return d.name; });
235
236     // vm name label
237     d3Vm.append('text')
238       .attr('x', defaultVmsWidth * 0.5)
239       .attr('y', defaultVmsHeight + 15)
240       .attr('text-anchor', 'middle')
241       .attr('class', 'linfolabel')
242       .text(function(d) { return d.name; });
243
244     // vm floating ip label
245     d3Vm.append('text')
246       .attr('x', -35)
247       .attr('y', 40)
248       .attr('text-anchor', 'middle')
249       .text(function(d) {
250         return (d.floatingIp) ? d.floatingIp.ip : '';
251       });
252
253   }
254
255   function findNetworkWithRouter(router) {
256     var result = [];
257     _.each(router.interfaces, function(inter) {
258       if (inter.type === 'router_interface') {
259         var net  = tmpNetHolder[inter.networkId] || null;
260
261         if (net) {
262           result.push({network: net, interface: inter});
263         }
264       }
265     });
266
267     return result;
268   }
269
270   function positionateNetwork(network, x, y, margin) {
271     network.x = x;
272     network.y = y;
273     margin = margin || 0;
274     network.color = d3.hsl(randomize.next() * 360, 1, 0.6).toString();
275
276     // look is the network is the highest
277     bbox.height = network.y > bbox.height ? network.y : bbox.height ;
278     bbox.width = network.x > bbox.width ? network.x : bbox.width;
279
280     // get the number of "childs" (router, vm)
281     var nbRouter = network.routers.length;
282     var nbVm = network.instances.length;
283
284     if (!network.external) {
285       _.each(network.subnets, function(subnet, i) {
286         network.ip += subnet.cidr;
287         if (i < network.subnets.length -1) {
288             network.ip += ', ';
289         }
290       });
291     }
292
293     // if needed, ajust the height of the network
294     // to be able to display all children
295     ajustHeighBaseOnChilds(nbRouter, nbVm);
296
297     var py = positionateRouter(network, x + routerMargin.width, y + margin);
298
299     positionateVm(network, x + vmMargin.width, py + 35 + margin);
300     delete tmpNetHolder[network.id];
301   }
302
303   function positionateRouter(network, x, y) {
304     var px = x,
305       py = y ;
306
307     // loop over all routers
308     _.each(network.routers, function(router, i) {
309       router.x = getRouterCentroid(x, py).x ;
310       router.y = py;
311       py += getRouterMarginHeight();
312
313       if (network.external) {
314         // find network ip with the gateway ip
315         var gateway = router.externalGateway.external_fixed_ips[0].ip_address;
316         var netIp = gateway.slice(0, gateway.lastIndexOf('.')) + '.0';
317         network.ip = netIp;
318       }
319
320       // look is the router is the highest
321       bbox.height = router.y > bbox.height ? router.y : bbox.height ;
322       bbox.width = router.x > bbox.width ? router.x : bbox.width;
323
324       linkData.push({
325         source: {x: network.x + (nodeWidth * 0.5), y: router.y + (defaultRouterHeight * 0.5)},
326         target: {x: router.x + (defaultRouterWidth * 0.5), y: router.y + (nodeWidth * 0.5)},
327         color:  network.color,
328         text: router.externalGateway.external_fixed_ips[0].ip_address
329       });
330
331       // go to the next layer
332       var nets = findNetworkWithRouter(router),
333         step = defaultRouterHeight / (nets.length + 1);
334
335       _.forEach(nets, function(net, i) {
336         var netPos = getNetworkLayerPosition(bbox.width + defaultRouterWidth);
337
338         positionateNetwork(net.network, netPos.x, netPos.y);
339         linkData.push({
340           source: {x: router.x + (2 * nodeWidth), y: router.y + step * (i + 1) },
341           target: {x: net.network.x + (nodeWidth * 0.5), y: router.y + (nodeWidth * 0.5 )},
342           color: net.network.color,
343           text: net.interface.ip.ip_address
344         });
345       });
346     });
347     return py;
348   }
349
350   function positionateVm(network, x, y) {
351
352     // I do vm before router because router
353     // will step to another BUS
354     _.each(network.instances, function(vm) {
355       vm.x = x;
356       vm.y = y;
357
358       // look is the network is the highest
359       bbox.height = vm.y > bbox.height ? vm.y : bbox.height ;
360       bbox.width = vm.x > bbox.width ? vm.x : bbox.width;
361
362       y += getVmMarginHeight();
363       linkData.push({
364         source: {x: network.x + (nodeWidth * 0.5), y: vm.y + (defaultVmsHeight * 0.5 )},
365         target: {x: vm.x + (defaultVmsWidth * 0.5), y: vm.y + (nodeWidth * 0.5)},
366         color:  network.color,
367         text: vm.ip
368       });
369     });
370   }
371
372   /*
373   *  Scan the whole "BUS" to display it properly
374   * ------------------------------------------------
375   *  I build it in a virtual space, if it need to be
376   *  resize it at the end when the overal bounding
377   *  box is known
378   */
379   function setTopologyPosition(networks) {
380     _.each(networks, function(net) {
381       tmpNetHolder[net.id] = net;
382     });
383
384     var i = 0;
385     for(var key in tmpNetHolder) {
386         var margin = (i === 0) ? 5 : networkMargin.width,
387           net = tmpNetHolder[key];
388         if (net.routers.length > 0) {
389           positionateNetwork(net, bbox.x + bbox.width + margin, bbox.y);
390           ++i;
391         }
392     }
393
394     for(var key in tmpNetHolder) {
395         var margin = networkMargin.width,
396           net = tmpNetHolder[key];
397           positionateNetwork(net, bbox.x + bbox.width + margin, bbox.y);
398     }
399   }
400
401   /*
402   * Check and ajust the height for a network.
403   */
404   function ajustHeighBaseOnChilds(nbRouter, nbVm) {
405     // calculate the height for the number of childs
406     var childHeight = nbRouter * (getRouterMarginHeight()) +
407       nbVm * (getVmMarginHeight()) + ipNetworkTextMaxLength;
408
409     // if heigh bigger than the default network height resize it
410     if (childHeight > nodeHeight) {
411       nodeHeight = childHeight + networkOffset;
412     }
413   }
414
415   /*
416   * Set the view to the modal position
417   */
418   function update() {
419
420     d3Node.attr('transform', function(d) {
421       return OvsCore.Util.String.Format("translate({0}, {1})",
422         d.x, d.y
423       );
424     });
425
426     d3Router.attr('transform', function(d, i) {
427       return OvsCore.Util.String.Format("translate({0}, {1})",
428         d.x, d.y
429       );
430     });
431
432     d3Vm.attr('transform', function(d) {
433       return OvsCore.Util.String.Format("translate({0}, {1})",
434         d.x, d.y
435       );
436     });
437
438     d3Link.attr('transform', function(d) {
439       return OvsCore.Util.String.Format("translate({0}, {1})",
440         d.source.x, d.source.y
441       );
442     });
443
444     // resize the graph if bigger than the canvas
445     var bbox = root.node().getBBox();
446     if (bbox.width > canvasWidth || bbox.height > canvasHeight) {
447       var sx = (canvasWidth - 30) / bbox.width,
448         sy = (canvasHeight - 30) / bbox.height,
449         s = sx < sy ? sx : sy;
450       d3.select('.layer_0').attr('transform', 'scale(' + s + ')');
451       console.log(root.node().getBBox());
452     }
453   }
454
455   function getRouterCentroid(x, y) {
456     return {
457       x : x + defaultRouterWidth * 0.5,
458       y : y + defaultRouterHeight * 0.5
459     };
460   }
461
462   function getRouterMarginHeight() {
463     return (defaultRouterHeight + routerMargin.height);
464   }
465
466   function getVmMarginHeight() {
467     return (defaultVmsHeight + vmMargin.height);
468   }
469
470   function getNetworkLayerPosition(x) {
471     return {
472       x : x + networkMargin.width,
473       y : networkMargin.height
474     };
475   }
476
477   function getVmLayerPosition(nbRouter, x) {
478     var t =  {
479       x : x + vmMargin.width * 2,
480       y : getRoutersDim(nbRouter).height + getVmMarginHeight()
481     };
482     return t;
483   }
484
485   LogicalGraph.prototype.onClick = _.noop;
486   LogicalGraph.prototype.dblClick = _.noop;
487
488   return LogicalGraph;
489 });