blob: 474120f0f9092b151be3022a9daf347e0d6ff97c [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;
28
29 // debounced functions
30 private renderGraph;
31
32 constructor(
33 private $log: ng.ILogService,
34 private XosServiceGraphStore: IXosServiceGraphStore,
Matteo Scandolo520a8a12017-03-10 17:31:37 -080035 private XosDebouncer: IXosDebouncer,
36 private XosModelDiscoverer: IXosModelDiscovererService,
37 private XosSidePanel: IXosSidePanelService
Matteo Scandolo75171782017-03-08 14:17:01 -080038 ) {
39 this.handleSvg();
40 this.setupForceLayout();
41 this.renderGraph = this.XosDebouncer.debounce(this._renderGraph, 500, this);
42
43 $(window).on('resize', () => {
44 this.setupForceLayout();
45 this.renderGraph();
46 });
47
48 this.GraphSubscription = this.XosServiceGraphStore.get()
49 .subscribe(
50 (graph) => {
51
52 if (!graph.nodes || graph.nodes.length === 0 || !graph.links || graph.links.length === 0) {
53 return;
54 }
55
56 this.$log.debug(`[XosFineGrainedTenancyGraphCtrl] Coarse Event and render`, graph);
57 this.graph = graph;
58 this.renderGraph();
59 },
60 (err) => {
61 this.$log.error(`[XosFineGrainedTenancyGraphCtrl] Error: `, err);
62 }
63 );
64 }
65
66 $onDestroy() {
67 this.GraphSubscription.unsubscribe();
68 }
69
70 private _renderGraph() {
71 this.addNodeLinksToForceLayout(this.graph);
72 this.renderNodes(this.graph.nodes);
73 this.renderLinks(this.graph.links);
74 }
75
76 private getSvgDimensions(): {width: number, heigth: number} {
77 return {
78 width: $('xos-fine-grained-tenancy-graph svg').width(),
79 heigth: $('xos-fine-grained-tenancy-graph svg').height()
80 };
81 }
82
83 private handleSvg() {
84 this.svg = d3.select('svg');
85
86 this.linkGroup = this.svg.append('g')
87 .attr({
88 class: 'link-group'
89 });
90
91 this.nodeGroup = this.svg.append('g')
92 .attr({
93 class: 'node-group'
94 });
95 }
96
97 private setupForceLayout() {
98
99 const tick = () => {
100 this.nodeGroup.selectAll('g.node')
101 .attr({
102 transform: d => `translate(${d.x}, ${d.y})`
103 });
104
105 this.linkGroup.selectAll('line')
106 .attr({
107 x1: l => l.source.x || 0,
108 y1: l => l.source.y || 0,
109 x2: l => l.target.x || 0,
110 y2: l => l.target.y || 0,
111 });
112 };
113 const svgDim = this.getSvgDimensions();
114 this.forceLayout = d3.layout.force()
115 .size([svgDim.width, svgDim.heigth])
116 .linkDistance(config.force.linkDistance)
117 .charge(config.force.charge)
118 .gravity(config.force.gravity)
119 .on('tick', tick);
120 }
121
122 private addNodeLinksToForceLayout(data: IXosServiceGraph) {
123 this.forceLayout
124 .nodes(data.nodes)
125 .links(data.links)
126 .start();
127 }
128
129 private getSiblingTextBBox(contex: any /* D3 this */) {
130 return d3.select(contex.parentNode).select('text').node().getBBox();
131 }
132
133 private renderServiceNodes(nodes: any) {
134
135 const self = this;
136 nodes.append('rect')
137 .attr({
138 rx: config.node.radius,
139 ry: config.node.radius
140 });
141
142 nodes.append('text')
143 .attr({
144 'text-anchor': 'middle'
145 })
146 .text(n => n.label);
147 // .text(n => `${n.id} - ${n.label}`);
148
149 const existing = nodes.selectAll('rect');
150
151 // resize node > rect as contained text
152 existing.each(function() {
153 const textBBox = self.getSiblingTextBBox(this);
154 const rect = d3.select(this);
155 rect.attr({
156 width: textBBox.width + config.node.padding,
157 height: textBBox.height + config.node.padding,
158 x: textBBox.x - (config.node.padding / 2),
159 y: textBBox.y - (config.node.padding / 2)
160 });
161 });
162 }
163
164 private renderTenantNodes(nodes: any) {
165 nodes.append('rect')
166 .attr({
167 width: 40,
168 height: 40,
169 x: -25,
170 y: -25,
171 transform: `rotate(45)`
172 });
173
174 nodes.append('text')
175 .attr({
176 'text-anchor': 'middle'
177 })
178 .text(n => n.label);
179 }
180
181 private renderNetworkNodes(nodes: any) {
182 const self = this;
183 nodes.append('circle');
184
185 nodes.append('text')
186 .attr({
187 'text-anchor': 'middle'
188 })
189 .text(n => n.label);
190
191 const existing = nodes.selectAll('circle');
192
193 // resize node > rect as contained text
194 existing.each(function() {
195 const textBBox = self.getSiblingTextBBox(this);
196 const rect = d3.select(this);
197 rect.attr({
198 r: (textBBox.width / 2) + config.node.padding,
199 cy: - (textBBox.height / 4)
200 });
201 });
202 }
203
204 private renderSubscriberNodes(nodes: any) {
205 const self = this;
206 nodes.append('rect');
207
208 nodes.append('text')
209 .attr({
210 'text-anchor': 'middle'
211 })
212 .text(n => n.label);
213
214 const existing = nodes.selectAll('rect');
215
216 // resize node > rect as contained text
217 existing.each(function() {
218 const textBBox = self.getSiblingTextBBox(this);
219 const rect = d3.select(this);
220 rect.attr({
221 width: textBBox.width + config.node.padding,
222 height: textBBox.height + config.node.padding,
223 x: textBBox.x - (config.node.padding / 2),
224 y: textBBox.y - (config.node.padding / 2)
225 });
226 });
227 }
228
229 private renderNodes(nodes: IXosServiceGraphNode[]) {
230 const node = this.nodeGroup
231 .selectAll('g.node')
232 .data(nodes, n => n.id);
233
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800234 let mouseEventsTimer, selectedModel;
Matteo Scandolo75171782017-03-08 14:17:01 -0800235 const svgDim = this.getSvgDimensions();
236 const hStep = svgDim.width / (nodes.length - 1);
237 const vStep = svgDim.heigth / (nodes.length - 1);
238 const entering = node.enter()
239 .append('g')
240 .attr({
241 class: n => `node ${n.type}`,
242 transform: (n, i) => `translate(${hStep * i}, ${vStep * i})`
243 })
244 .call(this.forceLayout.drag)
245 .on('mousedown', () => {
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800246 mouseEventsTimer = new Date().getTime();
Matteo Scandolo75171782017-03-08 14:17:01 -0800247 d3.event.stopPropagation();
248 })
Matteo Scandolo520a8a12017-03-10 17:31:37 -0800249 .on('mouseup', (n) => {
250 mouseEventsTimer = new Date().getTime() - mouseEventsTimer;
251 n.fixed = true;
252 })
253 .on('click', (n: IXosServiceGraphNode) => {
254 if (mouseEventsTimer > 100) {
255 // it is a drag
256 return;
257 }
258 if (selectedModel === n.id) {
259 // this model is already selected, so close the panel
260 this.XosSidePanel.removeInjectedComponents();
261 selectedModel = null;
262 return;
263 }
264 selectedModel = n.id;
265 const modelName = n.model['class_names'].split(',')[0];
266 const formConfig = this.XosModelDiscoverer.get(modelName).formCfg;
267 const model = angular.copy(n.model);
268 delete model.d3Id;
269 this.XosSidePanel.injectComponent('xosForm', {config: formConfig, ngModel: model});
Matteo Scandolo75171782017-03-08 14:17:01 -0800270 });
271
272 this.renderServiceNodes(entering.filter('.service'));
273 this.renderTenantNodes(entering.filter('.tenant'));
274 this.renderNetworkNodes(entering.filter('.network'));
275 this.renderSubscriberNodes(entering.filter('.subscriber'));
276 }
277
278 private renderLinks(links: IXosServiceGraphLink[]) {
279 const link = this.linkGroup
280 .selectAll('line')
281 .data(links, l => l.id);
282
283 const entering = link.enter();
284
285 entering.append('line')
286 .attr({
287 class: 'link',
288 'marker-start': 'url(#arrow)'
289 });
290 }
291}
292
293export const XosFineGrainedTenancyGraph: angular.IComponentOptions = {
294 template: require('./fine-grained.component.html'),
295 controllerAs: 'vm',
296 controller: XosFineGrainedTenancyGraphCtrl,
297};