blob: 5647c75d86a1dc33341cb03b6f57cd4cd5dd6e2d [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 Scandolo75171782017-03-08 14:17:01 -080031
32 // debounced functions
33 private renderGraph;
34
35 constructor(
36 private $log: ng.ILogService,
37 private XosServiceGraphStore: IXosServiceGraphStore,
Matteo Scandolo520a8a12017-03-10 17:31:37 -080038 private XosDebouncer: IXosDebouncer,
39 private XosModelDiscoverer: IXosModelDiscovererService,
Matteo Scandolo7629cc42017-03-13 14:12:15 -070040 private XosSidePanel: IXosSidePanelService,
41 private XosGraphHelpers: IXosGraphHelpers
Matteo Scandolo75171782017-03-08 14:17:01 -080042 ) {
43 this.handleSvg();
Simon Huntc8f23142017-03-14 14:11:13 -070044 this.loadDefs();
Matteo Scandolo75171782017-03-08 14:17:01 -080045 this.setupForceLayout();
46 this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 500, this);
47
48 $(window).on('resize', () => {
49 this.setupForceLayout();
50 this.renderGraph();
51 });
52
53 this.GraphSubscription = this.XosServiceGraphStore.get()
54 .subscribe(
55 (graph) => {
Matteo Scandolo98b5f5d2017-03-17 17:09:05 -070056 this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Fine-Grained Event and render`, graph);
Matteo Scandolo75171782017-03-08 14:17:01 -080057
58 if (!graph.nodes || graph.nodes.length === 0 || !graph.links || graph.links.length === 0) {
59 return;
60 }
61
Matteo Scandolo75171782017-03-08 14:17:01 -080062 this.graph = graph;
63 this.renderGraph();
64 },
65 (err) => {
66 this.$log.error(`[XosFineGrainedTenancyGraphCtrl] Error: `, err);
67 }
68 );
69 }
70
71 $onDestroy() {
72 this.GraphSubscription.unsubscribe();
73 }
74
75 private _renderGraph() {
76 this.addNodeLinksToForceLayout(this.graph);
77 this.renderNodes(this.graph.nodes);
78 this.renderLinks(this.graph.links);
79 }
80
81 private getSvgDimensions(): {width: number, heigth: number} {
82 return {
83 width: $('xos-fine-grained-tenancy-graph svg').width(),
84 heigth: $('xos-fine-grained-tenancy-graph svg').height()
85 };
86 }
87
88 private handleSvg() {
89 this.svg = d3.select('svg');
90
Simon Huntc8f23142017-03-14 14:11:13 -070091 this.defs = this.svg.append('defs');
92
Matteo Scandolo75171782017-03-08 14:17:01 -080093 this.linkGroup = this.svg.append('g')
94 .attr({
95 class: 'link-group'
96 });
97
98 this.nodeGroup = this.svg.append('g')
99 .attr({
100 class: 'node-group'
101 });
102 }
103
Simon Huntc8f23142017-03-14 14:11:13 -0700104 private loadDefs() {
105 const cloud = {
106 vbox: '0 0 303.8 185.8',
107 path: `M88.6,44.3c31.7-45.5,102.1-66.7,135-3
108 M37.8,142.9c-22.5,3.5-60.3-32.4-16.3-64.2
109 M101.8,154.2c-15.6,59.7-121.4,18.8-77.3-13
110 M194.6,150c-35.4,51.8-85.7,34.3-98.8-9.5
111 M274.4,116.4c29.4,73.2-81.9,80.3-87.7,44.3
112 M28.5,89.2C3.7,77.4,55.5,4.8,95.3,36.1
113 M216.1,28.9C270.9-13,340.8,91,278.4,131.1`,
114 bgpath: `M22,78.3C21.5,55.1,62.3,10.2,95.2,36
115 h0c31.9-33.4,88.1-50.5,120.6-7.2l0.3,0.2
116 C270.9-13,340.8,91,278.4,131.1v-0.5
117 c10.5,59.8-86.4,63.7-91.8,30.1h-0.4
118 c-30.2,33.6-67.6,24-84.6-6v-0.4
119 c-15.6,59.7-121.4,18.8-77.3-13
120 l-0.2-.2c-20.2-7.9-38.6-36.5-2.8-62.3Z`
121 };
122
123 this.defs.append('symbol')
124 .attr({ id: 'cloud', viewBox: cloud.vbox })
125 .append('path').attr('d', cloud.path);
126
127 this.defs.append('symbol')
128 .attr({ id: 'cloud_bg', viewBox: cloud.vbox })
129 .append('path').attr('d', cloud.bgpath);
130 }
131
Matteo Scandolo75171782017-03-08 14:17:01 -0800132 private setupForceLayout() {
133
134 const tick = () => {
135 this.nodeGroup.selectAll('g.node')
136 .attr({
137 transform: d => `translate(${d.x}, ${d.y})`
138 });
139
140 this.linkGroup.selectAll('line')
141 .attr({
142 x1: l => l.source.x || 0,
143 y1: l => l.source.y || 0,
144 x2: l => l.target.x || 0,
145 y2: l => l.target.y || 0,
146 });
147 };
148 const svgDim = this.getSvgDimensions();
149 this.forceLayout = d3.layout.force()
150 .size([svgDim.width, svgDim.heigth])
151 .linkDistance(config.force.linkDistance)
152 .charge(config.force.charge)
153 .gravity(config.force.gravity)
154 .on('tick', tick);
155 }
156
157 private addNodeLinksToForceLayout(data: IXosServiceGraph) {
158 this.forceLayout
159 .nodes(data.nodes)
160 .links(data.links)
161 .start();
162 }
163
Matteo Scandolo75171782017-03-08 14:17:01 -0800164 private renderServiceNodes(nodes: any) {
165
166 const self = this;
167 nodes.append('rect')
168 .attr({
169 rx: config.node.radius,
170 ry: config.node.radius
171 });
172
173 nodes.append('text')
174 .attr({
175 'text-anchor': 'middle'
176 })
177 .text(n => n.label);
178 // .text(n => `${n.id} - ${n.label}`);
179
180 const existing = nodes.selectAll('rect');
181
182 // resize node > rect as contained text
183 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700184 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Matteo Scandolo75171782017-03-08 14:17:01 -0800185 const rect = d3.select(this);
186 rect.attr({
187 width: textBBox.width + config.node.padding,
188 height: textBBox.height + config.node.padding,
189 x: textBBox.x - (config.node.padding / 2),
190 y: textBBox.y - (config.node.padding / 2)
191 });
192 });
193 }
194
195 private renderTenantNodes(nodes: any) {
196 nodes.append('rect')
197 .attr({
198 width: 40,
199 height: 40,
200 x: -25,
201 y: -25,
202 transform: `rotate(45)`
203 });
204
205 nodes.append('text')
206 .attr({
207 'text-anchor': 'middle'
208 })
209 .text(n => n.label);
210 }
211
212 private renderNetworkNodes(nodes: any) {
213 const self = this;
Simon Huntc8f23142017-03-14 14:11:13 -0700214 const yTextOff = 8;
215
216 nodes.append('use')
217 .attr({
218 class: 'symbol-bg',
219 'xlink:href': '#cloud_bg'
220 });
221
222 nodes.append('use')
223 .attr({
224 class: 'symbol',
225 'xlink:href': '#cloud'
226 });
Matteo Scandolo75171782017-03-08 14:17:01 -0800227
228 nodes.append('text')
229 .attr({
Simon Huntc8f23142017-03-14 14:11:13 -0700230 'text-anchor': 'middle',
231 'transform': 'translate(0,' + yTextOff + ')'
Matteo Scandolo75171782017-03-08 14:17:01 -0800232 })
233 .text(n => n.label);
234
Simon Huntc8f23142017-03-14 14:11:13 -0700235 const existing = nodes.selectAll('use');
Matteo Scandolo75171782017-03-08 14:17:01 -0800236
237 // resize node > rect as contained text
238 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700239 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Simon Huntc8f23142017-03-14 14:11:13 -0700240 const useElem = d3.select(this);
241 const w = textBBox.width + config.node.padding * 2;
242 const h = w;
243 const xoff = -(w / 2);
244 const yoff = -(h / 2);
245
246 useElem.attr({
247 width: w,
248 height: h,
249 transform: 'translate(' + xoff + ',' + yoff + ')'
Matteo Scandolo75171782017-03-08 14:17:01 -0800250 });
251 });
252 }
253
254 private renderSubscriberNodes(nodes: any) {
255 const self = this;
256 nodes.append('rect');
257
258 nodes.append('text')
259 .attr({
260 'text-anchor': 'middle'
261 })
262 .text(n => n.label);
263
264 const existing = nodes.selectAll('rect');
265
266 // resize node > rect as contained text
267 existing.each(function() {
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700268 const textBBox = self.XosGraphHelpers.getSiblingTextBBox(this);
Matteo Scandolo75171782017-03-08 14:17:01 -0800269 const rect = d3.select(this);
270 rect.attr({
271 width: textBBox.width + config.node.padding,
272 height: textBBox.height + config.node.padding,
273 x: textBBox.x - (config.node.padding / 2),
274 y: textBBox.y - (config.node.padding / 2)
275 });
276 });
277 }
278
279 private renderNodes(nodes: IXosServiceGraphNode[]) {
280 const node = this.nodeGroup
281 .selectAll('g.node')
282 .data(nodes, n => n.id);
283
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800284 let mouseEventsTimer, selectedModel;
Matteo Scandolo75171782017-03-08 14:17:01 -0800285 const svgDim = this.getSvgDimensions();
286 const hStep = svgDim.width / (nodes.length - 1);
287 const vStep = svgDim.heigth / (nodes.length - 1);
288 const entering = node.enter()
289 .append('g')
290 .attr({
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700291 class: n => `node ${n.type} ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
Matteo Scandolo75171782017-03-08 14:17:01 -0800292 transform: (n, i) => `translate(${hStep * i}, ${vStep * i})`
293 })
294 .call(this.forceLayout.drag)
295 .on('mousedown', () => {
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800296 mouseEventsTimer = new Date().getTime();
Matteo Scandolo75171782017-03-08 14:17:01 -0800297 d3.event.stopPropagation();
298 })
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800299 .on('mouseup', (n) => {
300 mouseEventsTimer = new Date().getTime() - mouseEventsTimer;
301 n.fixed = true;
302 })
303 .on('click', (n: IXosServiceGraphNode) => {
304 if (mouseEventsTimer > 100) {
305 // it is a drag
306 return;
307 }
308 if (selectedModel === n.id) {
309 // this model is already selected, so close the panel
310 this.XosSidePanel.removeInjectedComponents();
311 selectedModel = null;
312 return;
313 }
314 selectedModel = n.id;
315 const modelName = n.model['class_names'].split(',')[0];
316 const formConfig = this.XosModelDiscoverer.get(modelName).formCfg;
317 const model = angular.copy(n.model);
318 delete model.d3Id;
319 this.XosSidePanel.injectComponent('xosForm', {config: formConfig, ngModel: model});
Matteo Scandolo75171782017-03-08 14:17:01 -0800320 });
321
322 this.renderServiceNodes(entering.filter('.service'));
323 this.renderTenantNodes(entering.filter('.tenant'));
324 this.renderNetworkNodes(entering.filter('.network'));
325 this.renderSubscriberNodes(entering.filter('.subscriber'));
326 }
327
328 private renderLinks(links: IXosServiceGraphLink[]) {
329 const link = this.linkGroup
330 .selectAll('line')
331 .data(links, l => l.id);
332
333 const entering = link.enter();
334
335 entering.append('line')
336 .attr({
Matteo Scandolo7629cc42017-03-13 14:12:15 -0700337 class: n => `link ${this.XosGraphHelpers.parseElemClasses(n.d3Class)}`,
Matteo Scandolo75171782017-03-08 14:17:01 -0800338 'marker-start': 'url(#arrow)'
339 });
340 }
341}
342
343export const XosFineGrainedTenancyGraph: angular.IComponentOptions = {
344 template: require('./fine-grained.component.html'),
345 controllerAs: 'vm',
346 controller: XosFineGrainedTenancyGraphCtrl,
347};