Matteo Scandolo | d2044a4 | 2017-08-07 16:08:28 -0700 | [diff] [blame^] | 1 | |
| 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 Scandolo | 70bc45f | 2016-05-06 14:10:11 -0700 | [diff] [blame] | 19 | (function () { |
| 20 | 'use strict'; |
| 21 | |
| 22 | angular.module('xos.serviceGrid') |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 23 | .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 Scandolo | 70bc45f | 2016-05-06 14:10:11 -0700 | [diff] [blame] | 81 | .directive('serviceGraph', function(){ |
| 82 | return { |
| 83 | restrict: 'E', |
| 84 | scope: {}, |
| 85 | bindToController: true, |
| 86 | controllerAs: 'vm', |
| 87 | templateUrl: 'templates/service-graph.tpl.html', |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 88 | controller: function($scope, $element, GraphService, Graph, ToscaEncoder){ |
Matteo Scandolo | 70bc45f | 2016-05-06 14:10:11 -0700 | [diff] [blame] | 89 | |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 90 | let svg; |
| 91 | let el = $element[0]; |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 92 | let node, nodes; |
| 93 | let link, links; |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 94 | const _this = this; |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 95 | |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 96 | this.panelConfig = { |
| 97 | position: 'right' |
| 98 | }; |
| 99 | |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 100 | // 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 Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 116 | // animate node and links in the correct place |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 117 | const tick = () => { |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 118 | node |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 119 | .attr('cx', d => d.x) |
| 120 | .attr('cy', d => d.y) |
| 121 | .attr({ |
| 122 | transform: d => `translate(${d.x}, ${d.y})` |
| 123 | }); |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 124 | |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 125 | 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 Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 167 | }; |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 168 | |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 169 | Graph.getGraph().$promise |
| 170 | .then((graph) => { |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 171 | |
| 172 | // build links |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 173 | links = graph.edges().map(e => { |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 174 | return { |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 175 | source: graph.node(e.v), |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 176 | target: graph.node(e.w), |
| 177 | tenant: graph.edge(e) |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 178 | } |
| 179 | }); |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 180 | |
| 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 Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 184 | |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 185 | nodes = graph.nodes().reverse().map(n => getNodePosition(n, graph, longerGraph)); |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 186 | |
| 187 | handleSvg(el); |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 188 | defineArrows(); |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 189 | |
| 190 | var force = d3.layout.force() |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 191 | .nodes(nodes) |
| 192 | .links(links) |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 193 | .charge(-1060) |
| 194 | .gravity(0.1) |
| 195 | .linkDistance(200) |
| 196 | .size([el.clientWidth, el.clientHeight]) |
| 197 | .on('tick', tick) |
| 198 | .start(); |
| 199 | |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 200 | link = svg.selectAll('.link-container') |
| 201 | .data(links).enter().insert('g') |
| 202 | .attr('class', 'link-container'); |
| 203 | |
| 204 | link.insert('line') |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 205 | .attr('class', 'link') |
| 206 | .attr('marker-end', 'url(#arrow)'); |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 207 | |
Matteo Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 208 | 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 Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 217 | node = svg.selectAll('.node') |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 218 | .data(nodes) |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 219 | .enter().append('g') |
| 220 | .call(force.drag) |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 221 | .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 Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 244 | |
| 245 | node.append('circle') |
| 246 | .attr({ |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 247 | class: d => `node ${d.type || ''}`, |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 248 | r: 10 |
| 249 | }); |
| 250 | |
| 251 | node.append('text') |
| 252 | .attr({ |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 253 | 'text-anchor': 'middle', |
| 254 | 'alignment-baseline': 'middle' |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 255 | }) |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 256 | .text(d => d.humanReadableName || d.name); |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 257 | |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 258 | // scale the node to fit the contained text |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 259 | node.select('circle') |
| 260 | .attr({ |
| 261 | r: function(d){ |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 262 | 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 Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 269 | } |
| 270 | }) |
| 271 | |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 272 | }); |
Matteo Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 273 | |
| 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 Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 281 | }; |
| 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 Scandolo | 075f802 | 2016-08-23 09:10:37 -0700 | [diff] [blame] | 294 | .attr('class', 'link-arrow') |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 295 | .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 Scandolo | 819d13d | 2016-05-06 16:52:58 -0700 | [diff] [blame] | 313 | } |
Matteo Scandolo | 70bc45f | 2016-05-06 14:10:11 -0700 | [diff] [blame] | 314 | } |
| 315 | }; |
| 316 | }) |
Matteo Scandolo | e6393f0 | 2016-05-23 14:27:52 -0700 | [diff] [blame] | 317 | })(); |