blob: 0fbf411cf01ddd5f65903c4a0ad9e461eeeae2e3 [file] [log] [blame]
Matteo Scandolo75171782017-03-08 14:17:01 -08001import {IXosServiceGraphStore} from '../../services/graph.store';
2import './fine-grained.component.scss';
3import * as d3 from 'd3';
4import * as $ from 'jquery';
5import {Subscription} from 'rxjs';
6import {XosServiceGraphConfig as config} from '../../graph.config';
7import {IXosDebouncer} from '../../../core/services/helpers/debounce.helper';
8import {IXosServiceGraph, IXosServiceGraphLink, IXosServiceGraphNode} from '../../interfaces';
Matteo Scandolo520a8a12017-03-10 17:31:37 -08009import {IXosModelDiscovererService} from '../../../datasources/helpers/model-discoverer.service';
10import {IXosSidePanelService} from '../../../core/side-panel/side-panel.service';
Matteo Scandolo7629cc42017-03-13 14:12:15 -070011import {IXosGraphHelpers} from '../../services/d3-helpers/graph.helpers';
Matteo Scandolo75171782017-03-08 14:17:01 -080012
13class XosFineGrainedTenancyGraphCtrl {
14 static $inject = [
15 '$log',
16 'XosServiceGraphStore',
Matteo Scandolo520a8a12017-03-10 17:31:37 -080017 'XosDebouncer',
18 'XosModelDiscoverer',
Matteo Scandolo7629cc42017-03-13 14:12:15 -070019 'XosSidePanel',
20 'XosGraphHelpers'
Matteo Scandolo75171782017-03-08 14:17:01 -080021 ];
22
23 public graph: IXosServiceGraph;
24
25 private GraphSubscription: Subscription;
26 private svg;
27 private forceLayout;
28 private linkGroup;
29 private nodeGroup;
Simon Huntc8f23142017-03-14 14:11:13 -070030 private defs;
Matteo Scandolo6a7435f2017-03-24 18:07:17 -070031 private textSize = 20;
32 private textOffset = this.textSize / 4;
Matteo Scandolo75171782017-03-08 14:17:01 -080033
34 // debounced functions
35 private renderGraph;
36
37 constructor(
38 private $log: ng.ILogService,
39 private XosServiceGraphStore: IXosServiceGraphStore,
Matteo Scandolo520a8a12017-03-10 17:31:37 -080040 private XosDebouncer: IXosDebouncer,
41 private XosModelDiscoverer: IXosModelDiscovererService,
Matteo Scandolo7629cc42017-03-13 14:12:15 -070042 private XosSidePanel: IXosSidePanelService,
43 private XosGraphHelpers: IXosGraphHelpers
Matteo Scandolo75171782017-03-08 14:17:01 -080044 ) {
45 this.handleSvg();
Simon Huntc8f23142017-03-14 14:11:13 -070046 this.loadDefs();
Matteo Scandolo75171782017-03-08 14:17:01 -080047 this.setupForceLayout();
48 this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 500, this);
49
50 $(window).on('resize', () => {
51 this.setupForceLayout();
52 this.renderGraph();
53 });
54
55 this.GraphSubscription = this.XosServiceGraphStore.get()
56 .subscribe(
57 (graph) => {
Matteo Scandolo98b5f5d2017-03-17 17:09:05 -070058 this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Fine-Grained Event and render`, graph);
Matteo Scandolo75171782017-03-08 14:17:01 -080059
Matteo Scandolo265c2042017-03-20 10:15:40 -070060 if (!graph || !graph.nodes || !graph.links) {
Matteo Scandolo75171782017-03-08 14:17:01 -080061 return;
62 }
63
Matteo Scandolo75171782017-03-08 14:17:01 -080064 this.graph = graph;
65 this.renderGraph();
66 },
67 (err) => {
68 this.$log.error(`[XosFineGrainedTenancyGraphCtrl] Error: `, err);
69 }
70 );
71 }
72
73 $onDestroy() {
74 this.GraphSubscription.unsubscribe();
75 }
76
77 private _renderGraph() {
78 this.addNodeLinksToForceLayout(this.graph);
79 this.renderNodes(this.graph.nodes);
80 this.renderLinks(this.graph.links);
81 }
82
83 private getSvgDimensions(): {width: number, heigth: number} {
84 return {
85 width: $('xos-fine-grained-tenancy-graph svg').width(),
86 heigth: $('xos-fine-grained-tenancy-graph svg').height()
87 };
88 }
89
90 private handleSvg() {
91 this.svg = d3.select('svg');
92
Simon Huntc8f23142017-03-14 14:11:13 -070093 this.defs = this.svg.append('defs');
94
Matteo Scandolo75171782017-03-08 14:17:01 -080095 this.linkGroup = this.svg.append('g')
96 .attr({
97 class: 'link-group'
98 });
99
100 this.nodeGroup = this.svg.append('g')
101 .attr({
102 class: 'node-group'
103 });
104 }
105
Simon Huntc8f23142017-03-14 14:11:13 -0700106 private loadDefs() {
107 const cloud = {
108 vbox: '0 0 303.8 185.8',
109 path: `M88.6,44.3c31.7-45.5,102.1-66.7,135-3
110 M37.8,142.9c-22.5,3.5-60.3-32.4-16.3-64.2
111 M101.8,154.2c-15.6,59.7-121.4,18.8-77.3-13
112 M194.6,150c-35.4,51.8-85.7,34.3-98.8-9.5
113 M274.4,116.4c29.4,73.2-81.9,80.3-87.7,44.3
114 M28.5,89.2C3.7,77.4,55.5,4.8,95.3,36.1
115 M216.1,28.9C270.9-13,340.8,91,278.4,131.1`,
116 bgpath: `M22,78.3C21.5,55.1,62.3,10.2,95.2,36
117 h0c31.9-33.4,88.1-50.5,120.6-7.2l0.3,0.2
118 C270.9-13,340.8,91,278.4,131.1v-0.5
119 c10.5,59.8-86.4,63.7-91.8,30.1h-0.4
120 c-30.2,33.6-67.6,24-84.6-6v-0.4
121 c-15.6,59.7-121.4,18.8-77.3-13
122 l-0.2-.2c-20.2-7.9-38.6-36.5-2.8-62.3Z`
123 };
124
125 this.defs.append('symbol')
126 .attr({ id: 'cloud', viewBox: cloud.vbox })
127 .append('path').attr('d', cloud.path);
128
129 this.defs.append('symbol')
130 .attr({ id: 'cloud_bg', viewBox: cloud.vbox })
131 .append('path').attr('d', cloud.bgpath);
132 }
133
Matteo Scandolo75171782017-03-08 14:17:01 -0800134 private setupForceLayout() {
Matteo Scandolo0e8a8422017-03-25 14:55:40 -0700135 this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Setup Force Layout`);
Matteo Scandolo75171782017-03-08 14:17:01 -0800136 const tick = () => {
137 this.nodeGroup.selectAll('g.node')
138 .attr({
139 transform: d => `translate(${d.x}, ${d.y})`
140 });
141
142 this.linkGroup.selectAll('line')
143 .attr({
144 x1: l => l.source.x || 0,
145 y1: l => l.source.y || 0,
146 x2: l => l.target.x || 0,
147 y2: l => l.target.y || 0,
148 });
149 };
Matteo Scandolo0e8a8422017-03-25 14:55:40 -0700150 const getLinkStrenght = (l: IXosServiceGraphLink) => {
151 if (l.id.indexOf('service') > -1) {
152 return 0.1;
153 }
154 return 1;
155 };
Matteo Scandolo75171782017-03-08 14:17:01 -0800156 const svgDim = this.getSvgDimensions();
157 this.forceLayout = d3.layout.force()
158 .size([svgDim.width, svgDim.heigth])
159 .linkDistance(config.force.linkDistance)
Matteo Scandolo0e8a8422017-03-25 14:55:40 -0700160 .linkStrength(l => getLinkStrenght(l))
Matteo Scandolo75171782017-03-08 14:17:01 -0800161 .charge(config.force.charge)
162 .gravity(config.force.gravity)
163 .on('tick', tick);
164 }
165
166 private addNodeLinksToForceLayout(data: IXosServiceGraph) {
167 this.forceLayout
168 .nodes(data.nodes)
169 .links(data.links)
170 .start();
171 }
172
Matteo Scandolo75171782017-03-08 14:17:01 -0800173 private renderServiceNodes(nodes: any) {
174
175 const self = this;
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700176
Matteo Scandolo75171782017-03-08 14:17:01 -0800177 nodes.append('rect')
178 .attr({
179 rx: config.node.radius,
180 ry: config.node.radius
181 });
182
183 nodes.append('text')
184 .attr({
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700185 'text-anchor': 'middle',
186 'transform': `translate(0,${this.textOffset})`
Matteo Scandolo75171782017-03-08 14:17:01 -0800187 })
188 .text(n => n.label);
189 // .text(n => `${n.id} - ${n.label}`);
190
191 const existing = nodes.selectAll('rect');
192
193 // resize node > rect as contained text
194 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700195 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Matteo Scandolo75171782017-03-08 14:17:01 -0800196 const rect = d3.select(this);
197 rect.attr({
198 width: textBBox.width + config.node.padding,
199 height: textBBox.height + config.node.padding,
200 x: textBBox.x - (config.node.padding / 2),
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700201 y: (textBBox.y + self.textOffset) - (config.node.padding / 2)
Matteo Scandolo75171782017-03-08 14:17:01 -0800202 });
203 });
204 }
205
206 private renderTenantNodes(nodes: any) {
207 nodes.append('rect')
208 .attr({
209 width: 40,
210 height: 40,
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700211 x: -20,
212 y: -20,
Matteo Scandolo75171782017-03-08 14:17:01 -0800213 transform: `rotate(45)`
214 });
215
216 nodes.append('text')
217 .attr({
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700218 'text-anchor': 'middle',
219 'transform': `translate(0,${this.textOffset})`
Matteo Scandolo75171782017-03-08 14:17:01 -0800220 })
221 .text(n => n.label);
222 }
223
224 private renderNetworkNodes(nodes: any) {
225 const self = this;
Simon Huntc8f23142017-03-14 14:11:13 -0700226
227 nodes.append('use')
228 .attr({
229 class: 'symbol-bg',
230 'xlink:href': '#cloud_bg'
231 });
232
233 nodes.append('use')
234 .attr({
235 class: 'symbol',
236 'xlink:href': '#cloud'
237 });
Matteo Scandolo75171782017-03-08 14:17:01 -0800238
239 nodes.append('text')
240 .attr({
Simon Huntc8f23142017-03-14 14:11:13 -0700241 'text-anchor': 'middle',
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700242 'transform': `translate(0,${this.textOffset})`
Matteo Scandolo75171782017-03-08 14:17:01 -0800243 })
244 .text(n => n.label);
245
Simon Huntc8f23142017-03-14 14:11:13 -0700246 const existing = nodes.selectAll('use');
Matteo Scandolo75171782017-03-08 14:17:01 -0800247
248 // resize node > rect as contained text
249 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700250 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Simon Huntc8f23142017-03-14 14:11:13 -0700251 const useElem = d3.select(this);
252 const w = textBBox.width + config.node.padding * 2;
253 const h = w;
254 const xoff = -(w / 2);
255 const yoff = -(h / 2);
256
257 useElem.attr({
258 width: w,
259 height: h,
260 transform: 'translate(' + xoff + ',' + yoff + ')'
Matteo Scandolo75171782017-03-08 14:17:01 -0800261 });
262 });
263 }
264
265 private renderSubscriberNodes(nodes: any) {
266 const self = this;
267 nodes.append('rect');
268
269 nodes.append('text')
270 .attr({
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700271 'text-anchor': 'middle',
272 'transform': `translate(0,${this.textOffset})`
Matteo Scandolo75171782017-03-08 14:17:01 -0800273 })
274 .text(n => n.label);
275
276 const existing = nodes.selectAll('rect');
277
278 // resize node > rect as contained text
279 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700280 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Matteo Scandolo75171782017-03-08 14:17:01 -0800281 const rect = d3.select(this);
282 rect.attr({
283 width: textBBox.width + config.node.padding,
284 height: textBBox.height + config.node.padding,
285 x: textBBox.x - (config.node.padding / 2),
Matteo Scandolo6a7435f2017-03-24 18:07:17 -0700286 y: (textBBox.y + self.textOffset) - (config.node.padding / 2)
Matteo Scandolo75171782017-03-08 14:17:01 -0800287 });
288 });
289 }
290
291 private renderNodes(nodes: IXosServiceGraphNode[]) {
292 const node = this.nodeGroup
293 .selectAll('g.node')
294 .data(nodes, n => n.id);
295
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800296 let mouseEventsTimer, selectedModel;
Matteo Scandolo75171782017-03-08 14:17:01 -0800297 const svgDim = this.getSvgDimensions();
298 const hStep = svgDim.width / (nodes.length - 1);
299 const vStep = svgDim.heigth / (nodes.length - 1);
300 const entering = node.enter()
301 .append('g')
302 .attr({
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700303 class: n => `node ${n.type} ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
Matteo Scandolo75171782017-03-08 14:17:01 -0800304 transform: (n, i) => `translate(${hStep * i}, ${vStep * i})`
305 })
306 .call(this.forceLayout.drag)
307 .on('mousedown', () => {
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800308 mouseEventsTimer = new Date().getTime();
Matteo Scandolo75171782017-03-08 14:17:01 -0800309 d3.event.stopPropagation();
310 })
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800311 .on('mouseup', (n) => {
312 mouseEventsTimer = new Date().getTime() - mouseEventsTimer;
313 n.fixed = true;
314 })
315 .on('click', (n: IXosServiceGraphNode) => {
316 if (mouseEventsTimer > 100) {
317 // it is a drag
318 return;
319 }
320 if (selectedModel === n.id) {
321 // this model is already selected, so close the panel
322 this.XosSidePanel.removeInjectedComponents();
323 selectedModel = null;
324 return;
325 }
326 selectedModel = n.id;
327 const modelName = n.model['class_names'].split(',')[0];
328 const formConfig = this.XosModelDiscoverer.get(modelName).formCfg;
329 const model = angular.copy(n.model);
330 delete model.d3Id;
331 this.XosSidePanel.injectComponent('xosForm', {config: formConfig, ngModel: model});
Matteo Scandolo75171782017-03-08 14:17:01 -0800332 });
333
334 this.renderServiceNodes(entering.filter('.service'));
335 this.renderTenantNodes(entering.filter('.tenant'));
336 this.renderNetworkNodes(entering.filter('.network'));
337 this.renderSubscriberNodes(entering.filter('.subscriber'));
Matteo Scandolo0e8a8422017-03-25 14:55:40 -0700338 this.renderSubscriberNodes(entering.filter('.tenantroot'));
Matteo Scandolo75171782017-03-08 14:17:01 -0800339 }
340
341 private renderLinks(links: IXosServiceGraphLink[]) {
342 const link = this.linkGroup
343 .selectAll('line')
344 .data(links, l => l.id);
345
346 const entering = link.enter();
347
348 entering.append('line')
349 .attr({
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700350 class: n => `link ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
Matteo Scandolo75171782017-03-08 14:17:01 -0800351 'marker-start': 'url(#arrow)'
352 });
353 }
354}
355
356export const XosFineGrainedTenancyGraph: angular.IComponentOptions = {
357 template: require('./fine-grained.component.html'),
358 controllerAs: 'vm',
359 controller: XosFineGrainedTenancyGraphCtrl,
360};