blob: 3b51a02bf4d1634e76d602c4103fda5fd0a8d742 [file] [log] [blame]
Matteo Scandolofb46ae62017-08-08 09:10:50 -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 Scandolo75171782017-03-08 14:17:01 -080019import './fine-grained.component.scss';
20import * as d3 from 'd3';
21import * as $ from 'jquery';
Matteo Scandolobafd8d62017-03-29 23:23:00 -070022import * as _ from 'lodash';
Matteo Scandolo75171782017-03-08 14:17:01 -080023import {Subscription} from 'rxjs';
24import {XosServiceGraphConfig as config} from '../../graph.config';
25import {IXosDebouncer} from '../../../core/services/helpers/debounce.helper';
26import {IXosServiceGraph, IXosServiceGraphLink, IXosServiceGraphNode} from '../../interfaces';
Matteo Scandolo520a8a12017-03-10 17:31:37 -080027import {IXosModelDiscovererService} from '../../../datasources/helpers/model-discoverer.service';
28import {IXosSidePanelService} from '../../../core/side-panel/side-panel.service';
Matteo Scandolo7629cc42017-03-13 14:12:15 -070029import {IXosGraphHelpers} from '../../services/d3-helpers/graph.helpers';
Matteo Scandolobafd8d62017-03-29 23:23:00 -070030import {IXosServiceGraphExtender, IXosServiceGraphReducer} from '../../services/graph.extender';
Matteo Scandolo72181592017-07-25 14:49:40 -070031import {IXosServiceInstanceGraphStore} from '../../services/service-instance.graph.store';
Matteo Scandolo75171782017-03-08 14:17:01 -080032
33class XosFineGrainedTenancyGraphCtrl {
34 static $inject = [
35 '$log',
Matteo Scandolo72181592017-07-25 14:49:40 -070036 'XosServiceInstanceGraphStore',
Matteo Scandolo520a8a12017-03-10 17:31:37 -080037 'XosDebouncer',
38 'XosModelDiscoverer',
Matteo Scandolo7629cc42017-03-13 14:12:15 -070039 'XosSidePanel',
Matteo Scandolobafd8d62017-03-29 23:23:00 -070040 'XosGraphHelpers',
41 'XosServiceGraphExtender'
Matteo Scandolo75171782017-03-08 14:17:01 -080042 ];
43
44 public graph: IXosServiceGraph;
45
46 private GraphSubscription: Subscription;
47 private svg;
48 private forceLayout;
49 private linkGroup;
50 private nodeGroup;
Simon Huntc8f23142017-03-14 14:11:13 -070051 private defs;
Matteo Scandolo6a7435f2017-03-24 18:07:17 -070052 private textSize = 20;
53 private textOffset = this.textSize / 4;
Matteo Scandolo75171782017-03-08 14:17:01 -080054
55 // debounced functions
56 private renderGraph;
57
58 constructor(
59 private $log: ng.ILogService,
Matteo Scandolo72181592017-07-25 14:49:40 -070060 private XosServiceInstanceGraphStore: IXosServiceInstanceGraphStore,
Matteo Scandolo520a8a12017-03-10 17:31:37 -080061 private XosDebouncer: IXosDebouncer,
62 private XosModelDiscoverer: IXosModelDiscovererService,
Matteo Scandolo7629cc42017-03-13 14:12:15 -070063 private XosSidePanel: IXosSidePanelService,
Matteo Scandolobafd8d62017-03-29 23:23:00 -070064 private XosGraphHelpers: IXosGraphHelpers,
65 private XosServiceGraphExtender: IXosServiceGraphExtender
Matteo Scandolo75171782017-03-08 14:17:01 -080066 ) {
67 this.handleSvg();
Simon Huntc8f23142017-03-14 14:11:13 -070068 this.loadDefs();
Matteo Scandolo75171782017-03-08 14:17:01 -080069 this.setupForceLayout();
Matteo Scandolobafd8d62017-03-29 23:23:00 -070070 this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 1000, this);
Matteo Scandolo75171782017-03-08 14:17:01 -080071
72 $(window).on('resize', () => {
73 this.setupForceLayout();
74 this.renderGraph();
75 });
76
Matteo Scandolo72181592017-07-25 14:49:40 -070077 this.GraphSubscription = this.XosServiceInstanceGraphStore.get()
Matteo Scandolo75171782017-03-08 14:17:01 -080078 .subscribe(
79 (graph) => {
Matteo Scandolo72181592017-07-25 14:49:40 -070080 this.$log.debug(`[XosServiceInstanceGraphStore] Fine-Grained Event and render`, graph);
Matteo Scandolo75171782017-03-08 14:17:01 -080081
Matteo Scandolo265c2042017-03-20 10:15:40 -070082 if (!graph || !graph.nodes || !graph.links) {
Matteo Scandolo75171782017-03-08 14:17:01 -080083 return;
84 }
85
Matteo Scandolobafd8d62017-03-29 23:23:00 -070086 _.forEach(this.XosServiceGraphExtender.getFinegrained(), (r: IXosServiceGraphReducer) => {
87 graph = r.reducer(graph);
88 });
89
Matteo Scandolo75171782017-03-08 14:17:01 -080090 this.graph = graph;
91 this.renderGraph();
92 },
93 (err) => {
94 this.$log.error(`[XosFineGrainedTenancyGraphCtrl] Error: `, err);
95 }
96 );
97 }
98
99 $onDestroy() {
100 this.GraphSubscription.unsubscribe();
101 }
102
103 private _renderGraph() {
Matteo Scandolo9b460042017-04-14 16:24:45 -0700104 if (!angular.isDefined(this.graph) || !angular.isDefined(this.graph.nodes) || !angular.isDefined(this.graph.links)) {
105 return;
106 }
Matteo Scandolo75171782017-03-08 14:17:01 -0800107 this.addNodeLinksToForceLayout(this.graph);
108 this.renderNodes(this.graph.nodes);
109 this.renderLinks(this.graph.links);
110 }
111
Max Chu2bfddde2017-06-29 13:41:52 -0700112 private getSvgDimensions(): {width: number, height: number} {
Matteo Scandolo75171782017-03-08 14:17:01 -0800113 return {
114 width: $('xos-fine-grained-tenancy-graph svg').width(),
Max Chu2bfddde2017-06-29 13:41:52 -0700115 height: $('xos-fine-grained-tenancy-graph svg').height()
Matteo Scandolo75171782017-03-08 14:17:01 -0800116 };
117 }
118
119 private handleSvg() {
120 this.svg = d3.select('svg');
121
Simon Huntc8f23142017-03-14 14:11:13 -0700122 this.defs = this.svg.append('defs');
123
Matteo Scandolo75171782017-03-08 14:17:01 -0800124 this.linkGroup = this.svg.append('g')
125 .attr({
126 class: 'link-group'
127 });
128
129 this.nodeGroup = this.svg.append('g')
130 .attr({
131 class: 'node-group'
132 });
133 }
134
Simon Huntc8f23142017-03-14 14:11:13 -0700135 private loadDefs() {
136 const cloud = {
137 vbox: '0 0 303.8 185.8',
138 path: `M88.6,44.3c31.7-45.5,102.1-66.7,135-3
139 M37.8,142.9c-22.5,3.5-60.3-32.4-16.3-64.2
140 M101.8,154.2c-15.6,59.7-121.4,18.8-77.3-13
141 M194.6,150c-35.4,51.8-85.7,34.3-98.8-9.5
142 M274.4,116.4c29.4,73.2-81.9,80.3-87.7,44.3
143 M28.5,89.2C3.7,77.4,55.5,4.8,95.3,36.1
144 M216.1,28.9C270.9-13,340.8,91,278.4,131.1`,
145 bgpath: `M22,78.3C21.5,55.1,62.3,10.2,95.2,36
146 h0c31.9-33.4,88.1-50.5,120.6-7.2l0.3,0.2
147 C270.9-13,340.8,91,278.4,131.1v-0.5
148 c10.5,59.8-86.4,63.7-91.8,30.1h-0.4
149 c-30.2,33.6-67.6,24-84.6-6v-0.4
150 c-15.6,59.7-121.4,18.8-77.3-13
151 l-0.2-.2c-20.2-7.9-38.6-36.5-2.8-62.3Z`
152 };
153
154 this.defs.append('symbol')
155 .attr({ id: 'cloud', viewBox: cloud.vbox })
156 .append('path').attr('d', cloud.path);
157
158 this.defs.append('symbol')
159 .attr({ id: 'cloud_bg', viewBox: cloud.vbox })
160 .append('path').attr('d', cloud.bgpath);
161 }
162
Matteo Scandolo75171782017-03-08 14:17:01 -0800163 private setupForceLayout() {
Matteo Scandolo0e8a8422017-03-25 14:55:40 -0700164 this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Setup Force Layout`);
Matteo Scandolo75171782017-03-08 14:17:01 -0800165 const tick = () => {
166 this.nodeGroup.selectAll('g.node')
167 .attr({
168 transform: d => `translate(${d.x}, ${d.y})`
169 });
170
171 this.linkGroup.selectAll('line')
172 .attr({
173 x1: l => l.source.x || 0,
174 y1: l => l.source.y || 0,
175 x2: l => l.target.x || 0,
176 y2: l => l.target.y || 0,
177 });
178 };
Matteo Scandolo0e8a8422017-03-25 14:55:40 -0700179 const getLinkStrenght = (l: IXosServiceGraphLink) => {
Matteo Scandolo0e8a8422017-03-25 14:55:40 -0700180 return 1;
181 };
Matteo Scandolo75171782017-03-08 14:17:01 -0800182 const svgDim = this.getSvgDimensions();
183 this.forceLayout = d3.layout.force()
Max Chu2bfddde2017-06-29 13:41:52 -0700184 .size([svgDim.width, svgDim.height])
Matteo Scandolo75171782017-03-08 14:17:01 -0800185 .linkDistance(config.force.linkDistance)
Matteo Scandolo0e8a8422017-03-25 14:55:40 -0700186 .linkStrength(l => getLinkStrenght(l))
Matteo Scandolo75171782017-03-08 14:17:01 -0800187 .charge(config.force.charge)
188 .gravity(config.force.gravity)
189 .on('tick', tick);
190 }
191
192 private addNodeLinksToForceLayout(data: IXosServiceGraph) {
193 this.forceLayout
194 .nodes(data.nodes)
195 .links(data.links)
196 .start();
197 }
198
Matteo Scandolo75171782017-03-08 14:17:01 -0800199 private renderServiceNodes(nodes: any) {
200
201 const self = this;
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700202
Matteo Scandolo75171782017-03-08 14:17:01 -0800203 nodes.append('rect')
204 .attr({
205 rx: config.node.radius,
206 ry: config.node.radius
207 });
208
209 nodes.append('text')
210 .attr({
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700211 'text-anchor': 'middle',
212 'transform': `translate(0,${this.textOffset})`
Matteo Scandolo75171782017-03-08 14:17:01 -0800213 })
214 .text(n => n.label);
215 // .text(n => `${n.id} - ${n.label}`);
216
217 const existing = nodes.selectAll('rect');
218
219 // resize node > rect as contained text
220 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700221 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Matteo Scandolo75171782017-03-08 14:17:01 -0800222 const rect = d3.select(this);
223 rect.attr({
224 width: textBBox.width + config.node.padding,
225 height: textBBox.height + config.node.padding,
226 x: textBBox.x - (config.node.padding / 2),
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700227 y: (textBBox.y + self.textOffset) - (config.node.padding / 2)
Matteo Scandolo75171782017-03-08 14:17:01 -0800228 });
229 });
230 }
231
232 private renderTenantNodes(nodes: any) {
233 nodes.append('rect')
234 .attr({
235 width: 40,
236 height: 40,
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700237 x: -20,
238 y: -20,
Matteo Scandolo75171782017-03-08 14:17:01 -0800239 transform: `rotate(45)`
240 });
241
242 nodes.append('text')
243 .attr({
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700244 'text-anchor': 'middle',
245 'transform': `translate(0,${this.textOffset})`
Matteo Scandolo75171782017-03-08 14:17:01 -0800246 })
247 .text(n => n.label);
248 }
249
250 private renderNetworkNodes(nodes: any) {
251 const self = this;
Simon Huntc8f23142017-03-14 14:11:13 -0700252
253 nodes.append('use')
254 .attr({
255 class: 'symbol-bg',
256 'xlink:href': '#cloud_bg'
257 });
258
259 nodes.append('use')
260 .attr({
261 class: 'symbol',
262 'xlink:href': '#cloud'
263 });
Matteo Scandolo75171782017-03-08 14:17:01 -0800264
265 nodes.append('text')
266 .attr({
Simon Huntc8f23142017-03-14 14:11:13 -0700267 'text-anchor': 'middle',
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700268 'transform': `translate(0,${this.textOffset})`
Matteo Scandolo75171782017-03-08 14:17:01 -0800269 })
270 .text(n => n.label);
271
Simon Huntc8f23142017-03-14 14:11:13 -0700272 const existing = nodes.selectAll('use');
Matteo Scandolo75171782017-03-08 14:17:01 -0800273
274 // resize node > rect as contained text
275 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700276 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Simon Huntc8f23142017-03-14 14:11:13 -0700277 const useElem = d3.select(this);
278 const w = textBBox.width + config.node.padding * 2;
279 const h = w;
280 const xoff = -(w / 2);
281 const yoff = -(h / 2);
282
283 useElem.attr({
284 width: w,
285 height: h,
286 transform: 'translate(' + xoff + ',' + yoff + ')'
Matteo Scandolo75171782017-03-08 14:17:01 -0800287 });
288 });
289 }
290
291 private renderSubscriberNodes(nodes: any) {
292 const self = this;
293 nodes.append('rect');
294
295 nodes.append('text')
296 .attr({
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700297 'text-anchor': 'middle',
298 'transform': `translate(0,${this.textOffset})`
Matteo Scandolo75171782017-03-08 14:17:01 -0800299 })
300 .text(n => n.label);
301
302 const existing = nodes.selectAll('rect');
303
304 // resize node > rect as contained text
305 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700306 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Matteo Scandolo75171782017-03-08 14:17:01 -0800307 const rect = d3.select(this);
308 rect.attr({
309 width: textBBox.width + config.node.padding,
310 height: textBBox.height + config.node.padding,
311 x: textBBox.x - (config.node.padding / 2),
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700312 y: (textBBox.y + self.textOffset) - (config.node.padding / 2)
Matteo Scandolo75171782017-03-08 14:17:01 -0800313 });
314 });
315 }
316
317 private renderNodes(nodes: IXosServiceGraphNode[]) {
318 const node = this.nodeGroup
319 .selectAll('g.node')
320 .data(nodes, n => n.id);
321
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800322 let mouseEventsTimer, selectedModel;
Matteo Scandolo75171782017-03-08 14:17:01 -0800323 const svgDim = this.getSvgDimensions();
324 const hStep = svgDim.width / (nodes.length - 1);
Max Chu2bfddde2017-06-29 13:41:52 -0700325 const vStep = svgDim.height / (nodes.length - 1);
Matteo Scandolo75171782017-03-08 14:17:01 -0800326 const entering = node.enter()
327 .append('g')
328 .attr({
Matteo Scandolo72181592017-07-25 14:49:40 -0700329 id: n => n.id,
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700330 class: n => `node ${n.type} ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
Matteo Scandolo75171782017-03-08 14:17:01 -0800331 transform: (n, i) => `translate(${hStep * i}, ${vStep * i})`
332 })
333 .call(this.forceLayout.drag)
334 .on('mousedown', () => {
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800335 mouseEventsTimer = new Date().getTime();
Matteo Scandolo75171782017-03-08 14:17:01 -0800336 d3.event.stopPropagation();
337 })
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800338 .on('mouseup', (n) => {
339 mouseEventsTimer = new Date().getTime() - mouseEventsTimer;
340 n.fixed = true;
341 })
342 .on('click', (n: IXosServiceGraphNode) => {
343 if (mouseEventsTimer > 100) {
344 // it is a drag
345 return;
346 }
347 if (selectedModel === n.id) {
348 // this model is already selected, so close the panel
349 this.XosSidePanel.removeInjectedComponents();
350 selectedModel = null;
351 return;
352 }
353 selectedModel = n.id;
354 const modelName = n.model['class_names'].split(',')[0];
355 const formConfig = this.XosModelDiscoverer.get(modelName).formCfg;
356 const model = angular.copy(n.model);
357 delete model.d3Id;
358 this.XosSidePanel.injectComponent('xosForm', {config: formConfig, ngModel: model});
Matteo Scandolo75171782017-03-08 14:17:01 -0800359 });
360
361 this.renderServiceNodes(entering.filter('.service'));
Matteo Scandolo72181592017-07-25 14:49:40 -0700362 this.renderTenantNodes(entering.filter('.serviceinstance'));
Matteo Scandolo75171782017-03-08 14:17:01 -0800363 this.renderNetworkNodes(entering.filter('.network'));
364 this.renderSubscriberNodes(entering.filter('.subscriber'));
Matteo Scandolo72181592017-07-25 14:49:40 -0700365 // this.renderSubscriberNodes(entering.filter('.tenantroot'));
Matteo Scandolo75171782017-03-08 14:17:01 -0800366 }
367
368 private renderLinks(links: IXosServiceGraphLink[]) {
369 const link = this.linkGroup
370 .selectAll('line')
371 .data(links, l => l.id);
372
373 const entering = link.enter();
374
375 entering.append('line')
376 .attr({
Matteo Scandolo72181592017-07-25 14:49:40 -0700377 id: n => n.id,
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700378 class: n => `link ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
Matteo Scandolo75171782017-03-08 14:17:01 -0800379 'marker-start': 'url(#arrow)'
380 });
381 }
382}
383
384export const XosFineGrainedTenancyGraph: angular.IComponentOptions = {
385 template: require('./fine-grained.component.html'),
386 controllerAs: 'vm',
387 controller: XosFineGrainedTenancyGraphCtrl,
388};