blob: 649be6deab01c7f96cea3c88c46d2829bae8ea53 [file] [log] [blame]
Matteo Scandolo70bc45f2016-05-06 14:10:11 -07001(function () {
2 'use strict';
3
4 angular.module('xos.serviceGrid')
Matteo Scandoloe6393f02016-05-23 14:27:52 -07005 .service('Graph', function($q, Tenants, Services, Subscribers){
6
7 let tenancyGraph = new graphlib.Graph();
8 let cached = false;
9
10 const buildGraph = () => {
11
12 let deferred = $q.defer();
13
14 $q.all([
15 Tenants.query().$promise,
16 Services.query().$promise,
17 Subscribers.query().$promise
18 ])
19 .then((res) => {
20 let [tenants, services, subscribers] = res;
21 // adding service nodes
22 services.forEach(s => tenancyGraph.setNode(s.id, angular.extend(s, {type: 'service'})));
23
24
25 // coarse tenant
26 tenants.filter(t => t.subscriber_service && t.provider_service)
27 .forEach(t => tenancyGraph.setEdge(t.subscriber_service, t.provider_service, t, t.name));
28
29 // fine grain tenant
30 // adding subscribers as nodes (to build fine grain graph)
31 // subscribers.forEach(s => tenancyGraph.setNode(`sub-${s.id}`, angular.extend(s, {type: 'subscriber'})));
32 // TODO
33 // - Find tenant that start from a subscriber
34 // - Follow the chain: from the first tenant follow where subscriber_tenant = tenant_id untill we cannot find any more tenant
35 // tenants.filter(t => t.subscriber_root && t.provider_service)
36 // .forEach(t => tenancyGraph.setEdge(`sub-${t.subscriber_root}`, t.provider_service, t, t.name));
37
38 deferred.resolve(tenancyGraph);
39 });
40
41 return deferred.promise;
42 };
43
44 this.getGraph = () => {
45 let deferred = $q.defer();
46
47 if(cached){
48 deferred.resolve(tenancyGraph);
49 }
50 else {
51 buildGraph()
52 .then((res) => {
53 cached = true;
54 deferred.resolve(res);
55 })
56 .catch(console.log);
57 }
58
59 return {$promise: deferred.promise};
60 };
61
62 })
Matteo Scandolo70bc45f2016-05-06 14:10:11 -070063 .directive('serviceGraph', function(){
64 return {
65 restrict: 'E',
66 scope: {},
67 bindToController: true,
68 controllerAs: 'vm',
69 templateUrl: 'templates/service-graph.tpl.html',
Matteo Scandoloe6393f02016-05-23 14:27:52 -070070 controller: function($scope, $element, GraphService, Graph, ToscaEncoder){
Matteo Scandolo70bc45f2016-05-06 14:10:11 -070071
Matteo Scandolo819d13d2016-05-06 16:52:58 -070072 let svg;
73 let el = $element[0];
Matteo Scandolo075f8022016-08-23 09:10:37 -070074 let node, nodes;
75 let link, links;
Matteo Scandoloe6393f02016-05-23 14:27:52 -070076 const _this = this;
Matteo Scandolo819d13d2016-05-06 16:52:58 -070077
Matteo Scandoloe6393f02016-05-23 14:27:52 -070078 this.panelConfig = {
79 position: 'right'
80 };
81
Matteo Scandolo075f8022016-08-23 09:10:37 -070082 // find position for link labels
83 const xpos = (source, target) => {
84 if (target.x > source.x) {
85 return (source.x + (target.x - source.x)/2); }
86 else {
87 return (target.x + (source.x - target.x)/2); }
88 }
89
90 const ypos = (source, target) => {
91 if (target.y > source.y) {
92 return Math.round(source.y + (target.y - source.y)/2);
93 }
94 else {
95 return Math.round(target.y + (source.y - target.y)/2); }
96 }
97
Matteo Scandoloe6393f02016-05-23 14:27:52 -070098 // animate node and links in the correct place
Matteo Scandolo075f8022016-08-23 09:10:37 -070099 const tick = () => {
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700100 node
Matteo Scandolo075f8022016-08-23 09:10:37 -0700101 .attr('cx', d => d.x)
102 .attr('cy', d => d.y)
103 .attr({
104 transform: d => `translate(${d.x}, ${d.y})`
105 });
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700106
Matteo Scandolo075f8022016-08-23 09:10:37 -0700107 svg.selectAll('.link')
108 .attr('x1', d => d.source.x)
109 .attr('y1', d => d.source.y)
110 .attr('x2', d => getTargetNodeCircumferencePoint(d)[0])
111 .attr('y2', d => getTargetNodeCircumferencePoint(d)[1]);
112
113 svg.selectAll('.link-text')
114 .attr('x', d => xpos(d.source, {x: getTargetNodeCircumferencePoint(d)[0], y: getTargetNodeCircumferencePoint(d)[1]}))
115 .attr('y', d => ypos(d.source, {x: getTargetNodeCircumferencePoint(d)[0], y: getTargetNodeCircumferencePoint(d)[1]}))
116 .attr('transform', d => {
117 let x = xpos(d.source, {x: getTargetNodeCircumferencePoint(d)[0], y: getTargetNodeCircumferencePoint(d)[1]});
118 let y = ypos(d.source, {x: getTargetNodeCircumferencePoint(d)[0], y: getTargetNodeCircumferencePoint(d)[1]});
119 return `rotate(-30, ${x}, ${y})`;
120 });
121 };
122
123 var chainCount = 1;
124 var spareCount = 1;
125
126 const getNodePosition = (n, graph, chainElements) => {
127 let node = graph.node(n);
128 const step = el.clientWidth / (chainElements + 1);
129
130 if(graph.nodeEdges(n).length > 0){
131 let pos = {
132 y: el.clientHeight / 4,
133 x: step * chainCount,
134 fixed: true
135 };
136 angular.extend(node, pos);
137 chainCount = chainCount + 1;
138 }
139 else {
140 let pos = {
141 y: (el.clientHeight / 2) + (el.clientHeight / 4),
142 x: (step + step / 2) * spareCount,
143 fixed: true
144 };
145 angular.extend(node, pos);
146 spareCount = spareCount + 1;
147 }
148 return node;
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700149 };
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700150
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700151 Graph.getGraph().$promise
152 .then((graph) => {
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700153
154 // build links
Matteo Scandolo075f8022016-08-23 09:10:37 -0700155 links = graph.edges().map(e => {
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700156 return {
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700157 source: graph.node(e.v),
Matteo Scandolo075f8022016-08-23 09:10:37 -0700158 target: graph.node(e.w),
159 tenant: graph.edge(e)
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700160 }
161 });
Matteo Scandolo075f8022016-08-23 09:10:37 -0700162
163 // check how many nodes are connected
164 // let longerGraph = graphlib.alg.components(graph).filter(g => g.length > 1);
165 const longerGraph = graphlib.alg.components(graph).reduce(function (a, b) { return a.length > b.length ? a : b; }).length;
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700166
Matteo Scandolo075f8022016-08-23 09:10:37 -0700167 nodes = graph.nodes().reverse().map(n => getNodePosition(n, graph, longerGraph));
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700168
169 handleSvg(el);
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700170 defineArrows();
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700171
172 var force = d3.layout.force()
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700173 .nodes(nodes)
174 .links(links)
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700175 .charge(-1060)
176 .gravity(0.1)
177 .linkDistance(200)
178 .size([el.clientWidth, el.clientHeight])
179 .on('tick', tick)
180 .start();
181
Matteo Scandolo075f8022016-08-23 09:10:37 -0700182 link = svg.selectAll('.link-container')
183 .data(links).enter().insert('g')
184 .attr('class', 'link-container');
185
186 link.insert('line')
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700187 .attr('class', 'link')
188 .attr('marker-end', 'url(#arrow)');
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700189
Matteo Scandolo075f8022016-08-23 09:10:37 -0700190 var linkText = svg.selectAll('.link-container')
191 .data(force.links())
192 .insert('text')
193 .attr({
194 class: 'link-text',
195 'text-anchor': 'start'
196 })
197 .text(d => `${d.tenant.humanReadableName}`)
198
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700199 node = svg.selectAll('.node')
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700200 .data(nodes)
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700201 .enter().append('g')
202 .call(force.drag)
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700203 .on('mousedown', function(d) {
204 $scope.$apply(() => {
205 if(d.name === 'XOS'){
206 return;
207 }
208 _this.panelShow = true;
209 let status = parseInt(d.backend_status.match(/^[0-9]/)[0]);
210 console.log(status);
211 switch(status){
212 case 0:
213 d.icon = 'time';
214 break;
215 case 1:
216 d.icon = 'ok';
217 break;
218 case 2:
219 d.icon = 'remove';
220 break;
221 }
222 _this.selectedNode = d;
223 });
224 d3.event.stopPropagation();
225 });
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700226
227 node.append('circle')
228 .attr({
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700229 class: d => `node ${d.type || ''}`,
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700230 r: 10
231 });
232
233 node.append('text')
234 .attr({
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700235 'text-anchor': 'middle',
236 'alignment-baseline': 'middle'
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700237 })
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700238 .text(d => d.humanReadableName || d.name);
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700239
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700240 // scale the node to fit the contained text
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700241 node.select('circle')
242 .attr({
243 r: function(d){
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700244 const parent = d3.select(this).node().parentNode;
245 const sib = d3.select(parent).select('text').node().getBBox();
246 const radius = (sib.width / 2) + 10;
247
248 // add radius as node attribute
249 d.nodeWidth = radius * 2;
250 return radius;
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700251 }
252 })
253
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700254 });
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700255
256 const handleSvg = (el) => {
257 d3.select(el).select('svg').remove();
258
259 svg = d3.select(el)
260 .append('svg')
261 .style('width', `${el.clientWidth}px`)
262 .style('height', `${el.clientHeight}px`);
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700263 };
264
265 const defineArrows = () => {
266 svg.append('svg:defs').selectAll('marker')
267 .data(['arrow']) // Different link/path types can be defined here
268 .enter().append('svg:marker') // This section adds in the arrows
269 .attr('id', String)
270 .attr('viewBox', '0 -5 10 10')
271 .attr('refX', 10)
272 .attr('refY', 0)
273 .attr('markerWidth', 6)
274 .attr('markerHeight', 6)
275 .attr('orient', 'auto')
Matteo Scandolo075f8022016-08-23 09:10:37 -0700276 .attr('class', 'link-arrow')
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700277 .append('svg:path')
278 .attr('d', 'M0,-5L10,0L0,5');
279 };
280
281 const getTargetNodeCircumferencePoint = d => {
282 const radius = d.target.nodeWidth / 2; // nodeWidth is just a custom attribute I calculate during the creation of the nodes depending on the node width
283 const dx = d.target.x - d.source.x;
284 const dy = d.target.y - d.source.y;
285 const gamma = Math.atan2(dy, dx); // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
286
287 const tx = d.target.x - (Math.cos(gamma) * radius);
288 const ty = d.target.y - (Math.sin(gamma) * radius);
289
290 return [tx, ty];
291 };
292
293 this.exportToTosca = (service) => {
294 ToscaEncoder.serviceToTosca(service);
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700295 }
Matteo Scandolo70bc45f2016-05-06 14:10:11 -0700296 }
297 };
298 })
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700299})();