blob: e2b29b4ff70a2810b05b2f48fb2b4046c20792ff [file] [log] [blame]
Matteo Scandolod2044a42017-08-07 16:08:28 -07001
2/*
3 * Copyright 2017-present Open Networking Foundation
4
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8
9 * http://www.apache.org/licenses/LICENSE-2.0
10
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18
Matteo Scandolo70bc45f2016-05-06 14:10:11 -070019(function () {
20 'use strict';
21
22 angular.module('xos.serviceGrid')
Matteo Scandoloe6393f02016-05-23 14:27:52 -070023 .service('Graph', function($q, Tenants, Services, Subscribers){
24
25 let tenancyGraph = new graphlib.Graph();
26 let cached = false;
27
28 const buildGraph = () => {
29
30 let deferred = $q.defer();
31
32 $q.all([
33 Tenants.query().$promise,
34 Services.query().$promise,
35 Subscribers.query().$promise
36 ])
37 .then((res) => {
38 let [tenants, services, subscribers] = res;
39 // adding service nodes
40 services.forEach(s => tenancyGraph.setNode(s.id, angular.extend(s, {type: 'service'})));
41
42
43 // coarse tenant
44 tenants.filter(t => t.subscriber_service && t.provider_service)
45 .forEach(t => tenancyGraph.setEdge(t.subscriber_service, t.provider_service, t, t.name));
46
47 // fine grain tenant
48 // adding subscribers as nodes (to build fine grain graph)
49 // subscribers.forEach(s => tenancyGraph.setNode(`sub-${s.id}`, angular.extend(s, {type: 'subscriber'})));
50 // TODO
51 // - Find tenant that start from a subscriber
52 // - Follow the chain: from the first tenant follow where subscriber_tenant = tenant_id untill we cannot find any more tenant
53 // tenants.filter(t => t.subscriber_root && t.provider_service)
54 // .forEach(t => tenancyGraph.setEdge(`sub-${t.subscriber_root}`, t.provider_service, t, t.name));
55
56 deferred.resolve(tenancyGraph);
57 });
58
59 return deferred.promise;
60 };
61
62 this.getGraph = () => {
63 let deferred = $q.defer();
64
65 if(cached){
66 deferred.resolve(tenancyGraph);
67 }
68 else {
69 buildGraph()
70 .then((res) => {
71 cached = true;
72 deferred.resolve(res);
73 })
74 .catch(console.log);
75 }
76
77 return {$promise: deferred.promise};
78 };
79
80 })
Matteo Scandolo70bc45f2016-05-06 14:10:11 -070081 .directive('serviceGraph', function(){
82 return {
83 restrict: 'E',
84 scope: {},
85 bindToController: true,
86 controllerAs: 'vm',
87 templateUrl: 'templates/service-graph.tpl.html',
Matteo Scandoloe6393f02016-05-23 14:27:52 -070088 controller: function($scope, $element, GraphService, Graph, ToscaEncoder){
Matteo Scandolo70bc45f2016-05-06 14:10:11 -070089
Matteo Scandolo819d13d2016-05-06 16:52:58 -070090 let svg;
91 let el = $element[0];
Matteo Scandolo075f8022016-08-23 09:10:37 -070092 let node, nodes;
93 let link, links;
Matteo Scandoloe6393f02016-05-23 14:27:52 -070094 const _this = this;
Matteo Scandolo819d13d2016-05-06 16:52:58 -070095
Matteo Scandoloe6393f02016-05-23 14:27:52 -070096 this.panelConfig = {
97 position: 'right'
98 };
99
Matteo Scandolo075f8022016-08-23 09:10:37 -0700100 // find position for link labels
101 const xpos = (source, target) => {
102 if (target.x > source.x) {
103 return (source.x + (target.x - source.x)/2); }
104 else {
105 return (target.x + (source.x - target.x)/2); }
106 }
107
108 const ypos = (source, target) => {
109 if (target.y > source.y) {
110 return Math.round(source.y + (target.y - source.y)/2);
111 }
112 else {
113 return Math.round(target.y + (source.y - target.y)/2); }
114 }
115
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700116 // animate node and links in the correct place
Matteo Scandolo075f8022016-08-23 09:10:37 -0700117 const tick = () => {
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700118 node
Matteo Scandolo075f8022016-08-23 09:10:37 -0700119 .attr('cx', d => d.x)
120 .attr('cy', d => d.y)
121 .attr({
122 transform: d => `translate(${d.x}, ${d.y})`
123 });
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700124
Matteo Scandolo075f8022016-08-23 09:10:37 -0700125 svg.selectAll('.link')
126 .attr('x1', d => d.source.x)
127 .attr('y1', d => d.source.y)
128 .attr('x2', d => getTargetNodeCircumferencePoint(d)[0])
129 .attr('y2', d => getTargetNodeCircumferencePoint(d)[1]);
130
131 svg.selectAll('.link-text')
132 .attr('x', d => xpos(d.source, {x: getTargetNodeCircumferencePoint(d)[0], y: getTargetNodeCircumferencePoint(d)[1]}))
133 .attr('y', d => ypos(d.source, {x: getTargetNodeCircumferencePoint(d)[0], y: getTargetNodeCircumferencePoint(d)[1]}))
134 .attr('transform', d => {
135 let x = xpos(d.source, {x: getTargetNodeCircumferencePoint(d)[0], y: getTargetNodeCircumferencePoint(d)[1]});
136 let y = ypos(d.source, {x: getTargetNodeCircumferencePoint(d)[0], y: getTargetNodeCircumferencePoint(d)[1]});
137 return `rotate(-30, ${x}, ${y})`;
138 });
139 };
140
141 var chainCount = 1;
142 var spareCount = 1;
143
144 const getNodePosition = (n, graph, chainElements) => {
145 let node = graph.node(n);
146 const step = el.clientWidth / (chainElements + 1);
147
148 if(graph.nodeEdges(n).length > 0){
149 let pos = {
150 y: el.clientHeight / 4,
151 x: step * chainCount,
152 fixed: true
153 };
154 angular.extend(node, pos);
155 chainCount = chainCount + 1;
156 }
157 else {
158 let pos = {
159 y: (el.clientHeight / 2) + (el.clientHeight / 4),
160 x: (step + step / 2) * spareCount,
161 fixed: true
162 };
163 angular.extend(node, pos);
164 spareCount = spareCount + 1;
165 }
166 return node;
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700167 };
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700168
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700169 Graph.getGraph().$promise
170 .then((graph) => {
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700171
172 // build links
Matteo Scandolo075f8022016-08-23 09:10:37 -0700173 links = graph.edges().map(e => {
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700174 return {
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700175 source: graph.node(e.v),
Matteo Scandolo075f8022016-08-23 09:10:37 -0700176 target: graph.node(e.w),
177 tenant: graph.edge(e)
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700178 }
179 });
Matteo Scandolo075f8022016-08-23 09:10:37 -0700180
181 // check how many nodes are connected
182 // let longerGraph = graphlib.alg.components(graph).filter(g => g.length > 1);
183 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 -0700184
Matteo Scandolo075f8022016-08-23 09:10:37 -0700185 nodes = graph.nodes().reverse().map(n => getNodePosition(n, graph, longerGraph));
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700186
187 handleSvg(el);
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700188 defineArrows();
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700189
190 var force = d3.layout.force()
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700191 .nodes(nodes)
192 .links(links)
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700193 .charge(-1060)
194 .gravity(0.1)
195 .linkDistance(200)
196 .size([el.clientWidth, el.clientHeight])
197 .on('tick', tick)
198 .start();
199
Matteo Scandolo075f8022016-08-23 09:10:37 -0700200 link = svg.selectAll('.link-container')
201 .data(links).enter().insert('g')
202 .attr('class', 'link-container');
203
204 link.insert('line')
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700205 .attr('class', 'link')
206 .attr('marker-end', 'url(#arrow)');
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700207
Matteo Scandolo075f8022016-08-23 09:10:37 -0700208 var linkText = svg.selectAll('.link-container')
209 .data(force.links())
210 .insert('text')
211 .attr({
212 class: 'link-text',
213 'text-anchor': 'start'
214 })
215 .text(d => `${d.tenant.humanReadableName}`)
216
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700217 node = svg.selectAll('.node')
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700218 .data(nodes)
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700219 .enter().append('g')
220 .call(force.drag)
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700221 .on('mousedown', function(d) {
222 $scope.$apply(() => {
223 if(d.name === 'XOS'){
224 return;
225 }
226 _this.panelShow = true;
227 let status = parseInt(d.backend_status.match(/^[0-9]/)[0]);
228 console.log(status);
229 switch(status){
230 case 0:
231 d.icon = 'time';
232 break;
233 case 1:
234 d.icon = 'ok';
235 break;
236 case 2:
237 d.icon = 'remove';
238 break;
239 }
240 _this.selectedNode = d;
241 });
242 d3.event.stopPropagation();
243 });
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700244
245 node.append('circle')
246 .attr({
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700247 class: d => `node ${d.type || ''}`,
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700248 r: 10
249 });
250
251 node.append('text')
252 .attr({
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700253 'text-anchor': 'middle',
254 'alignment-baseline': 'middle'
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700255 })
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700256 .text(d => d.humanReadableName || d.name);
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700257
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700258 // scale the node to fit the contained text
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700259 node.select('circle')
260 .attr({
261 r: function(d){
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700262 const parent = d3.select(this).node().parentNode;
263 const sib = d3.select(parent).select('text').node().getBBox();
264 const radius = (sib.width / 2) + 10;
265
266 // add radius as node attribute
267 d.nodeWidth = radius * 2;
268 return radius;
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700269 }
270 })
271
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700272 });
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700273
274 const handleSvg = (el) => {
275 d3.select(el).select('svg').remove();
276
277 svg = d3.select(el)
278 .append('svg')
279 .style('width', `${el.clientWidth}px`)
280 .style('height', `${el.clientHeight}px`);
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700281 };
282
283 const defineArrows = () => {
284 svg.append('svg:defs').selectAll('marker')
285 .data(['arrow']) // Different link/path types can be defined here
286 .enter().append('svg:marker') // This section adds in the arrows
287 .attr('id', String)
288 .attr('viewBox', '0 -5 10 10')
289 .attr('refX', 10)
290 .attr('refY', 0)
291 .attr('markerWidth', 6)
292 .attr('markerHeight', 6)
293 .attr('orient', 'auto')
Matteo Scandolo075f8022016-08-23 09:10:37 -0700294 .attr('class', 'link-arrow')
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700295 .append('svg:path')
296 .attr('d', 'M0,-5L10,0L0,5');
297 };
298
299 const getTargetNodeCircumferencePoint = d => {
300 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
301 const dx = d.target.x - d.source.x;
302 const dy = d.target.y - d.source.y;
303 const gamma = Math.atan2(dy, dx); // Math.atan2 returns the angle in the correct quadrant as opposed to Math.atan
304
305 const tx = d.target.x - (Math.cos(gamma) * radius);
306 const ty = d.target.y - (Math.sin(gamma) * radius);
307
308 return [tx, ty];
309 };
310
311 this.exportToTosca = (service) => {
312 ToscaEncoder.serviceToTosca(service);
Matteo Scandolo819d13d2016-05-06 16:52:58 -0700313 }
Matteo Scandolo70bc45f2016-05-06 14:10:11 -0700314 }
315 };
316 })
Matteo Scandoloe6393f02016-05-23 14:27:52 -0700317})();