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