Skip to content

Commit 9bd1887

Browse files
committed
Merge pull request #137 from vincent99/d3-graph
WIP graph view
2 parents e8c191e + 38abedc commit 9bd1887

8 files changed

Lines changed: 236 additions & 158 deletions

File tree

.jshintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"Terminal",
1111
"Prism",
1212
"Ui",
13-
"cytoscape"
13+
"dagreD3"
1414
],
1515
"browser" : true,
1616
"boss" : true,

Brocfile.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ app.import('bower_components/bootstrap-multiselect/dist/js/bootstrap-multiselect
5959
app.import('bower_components/bootstrap-multiselect/dist/css/bootstrap-multiselect.css');
6060
app.import('bower_components/prism/prism.js');
6161
app.import('bower_components/prism/components/prism-yaml.js');
62-
app.import('bower_components/cytoscape/dist/cytoscape.js');
63-
app.import('bower_components/cytoscape/lib/dagre.js');
62+
app.import('bower_components/lodash/dist/lodash.js');
63+
app.import('bower_components/graphlib/dist/graphlib.core.js');
64+
app.import('bower_components/dagre/dist/dagre.core.js');
65+
app.import('bower_components/dagre-d3/dist/dagre-d3.core.js');
6466

6567
module.exports = app.toTree();

app/components/page-nav/component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ export default Ember.Component.extend({
77
tagName: 'nav',
88
hasServices: function() {
99
var store = this.get('store');
10-
return store && store.hasRecordFor('schema','service');
10+
return store && store.hasRecordFor('schema','service') && this.get('session.showServices');
1111
}.property(),
1212
});

app/environment/controller.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,8 @@ var EnvironmentController = Cattle.TransitioningResourceController.extend({
2727

2828
exportConfig: function() {
2929
var auth = this.get('controllers.authenticated');
30-
31-
var compose = auth.addAuthParams(this.linkFor('dockerComposeConfig'));
32-
var rancher = auth.addAuthParams(this.linkFor('rancherComposeConfig'));
33-
34-
Util.download(compose);
35-
setTimeout(() => {
36-
// The 2nd download needs a different iframe id or it will overwrite the first before downloading
37-
Util.download(rancher, '__downloadIframe2');
38-
}, 250);
30+
var url = auth.addAuthParams(this.linkFor('composeConfig'));
31+
Util.download(url);
3932
},
4033
},
4134

app/environment/graph/view.js

Lines changed: 112 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,182 +1,153 @@
11
import Ember from 'ember';
2+
import Util from 'ui/utils/util';
23
import ThrottledResize from 'ui/mixins/throttled-resize';
34

45
export default Ember.View.extend(ThrottledResize,{
5-
cy: null,
6-
cyElem: null,
6+
graphZoom: null,
7+
graphInner: null,
8+
graphOuter: null,
9+
graphRender: null,
10+
graph: null,
11+
12+
didInsertElement: function() {
13+
this._super();
14+
var elem = $('<div id="environment-graph"><svg style="width: 100%; height: 100%;"><g/></svg></div>').appendTo('BODY');
15+
this.set('graphElem', elem[0]);
16+
17+
Ember.run.later(this,'initGraph',100);
18+
},
719

820
onResize: function() {
9-
var cy = this.get('cy');
10-
if ( cy )
21+
$('#environment-graph').css('top', $('MAIN').position().top + 55 + 'px');
22+
if ( this.get('graph') )
1123
{
12-
setTimeout(function() {
13-
cy.fit(null, 20);
14-
},100);
24+
this.renderGraph();
1525
}
1626
},
1727

18-
didInsertElement: function() {
19-
this._super();
20-
var elem = $('<div id="environment-graph"></div>').appendTo('BODY');
21-
elem.css('top', $('MAIN').position().top + $('MAIN').height() + 'px');
22-
this.set('cyElem', elem[0]);
28+
initGraph: function() {
29+
var outer = d3.select("#environment-graph svg");
30+
var inner = outer.select("g");
31+
var zoom = d3.behavior.zoom().on("zoom", function() {
32+
inner.attr("transform", "translate(" + d3.event.translate + ")" +
33+
"scale(" + d3.event.scale + ")");
34+
});
35+
36+
outer.call(zoom);
37+
38+
var g = new dagreD3.graphlib.Graph().setGraph({
39+
rankdir: "TB",
40+
nodesep: 50,
41+
ranksep: 50,
42+
marginx: 30,
43+
marginy: 30
44+
});
45+
46+
var render = new dagreD3.render();
2347

24-
Ember.run.later(this,'graph',250);
48+
this.setProperties({
49+
graphZoom: zoom,
50+
graphOuter: outer,
51+
graphInner: inner,
52+
graphRender: render,
53+
graph: g
54+
});
55+
56+
this.updateGraph();
2557
},
2658

2759
updateGraph: function() {
28-
console.log('starting updateGraph');
29-
var cy = this.get('cy');
30-
cy.startBatch();
31-
60+
var g = this.get('graph');
3261
var services = this.get('context.services');
33-
34-
var relayout = false;
35-
var startNodes = cy.nodes().length;
36-
var startEdges = cy.edges().length;
37-
var expectedNodes = cy.collection();
38-
var expectedEdges = cy.collection();
3962
var unremovedServices = services.filter(function(service) {
4063
return ['removed','purging','purged'].indexOf(service.get('state')) === -1;
4164
});
4265

66+
var unexpectedNodes = g.nodes();
67+
var unexpectedEdges = g.edges();
68+
4369
unremovedServices.forEach(function(service) {
4470
var serviceId = service.get('id');
45-
var node = cy.getElementById(serviceId)[0];
46-
if ( !node )
47-
{
48-
relayout = true;
49-
node = cy.add({
50-
group: 'nodes',
51-
data: {
52-
id: serviceId,
53-
shape: 'ellipse',
54-
},
55-
position: {x: 0, y: 0}
56-
})[0];
57-
}
58-
node.data({
59-
name: service.get('name'),
60-
color: (service.get('state') === 'active' ? '#41c77d' : (service.get('state') === 'inactive' ? '#f00' : '#f3bd24')),
71+
var color = (service.get('state') === 'active' ? 'green' : (service.get('state') === 'inactive' ? 'red' : 'yellow'));
72+
var instances = service.get('instances.length')||'No';
73+
74+
var html = '<i class="icon ss-layergroup"></i>' +
75+
'<h4>'+ Util.escapeHtml(service.get('name')) + '</h4>' +
76+
'<h6 class="count"><b>' + instances + '</b> container' + (instances === 1 ? '' : 's') + '</h6>' +
77+
'<h6><span class="state '+ color +'">' + Util.escapeHtml(Util.ucFirst(service.get('state'))) + '</span></h6>';
78+
79+
g.setNode(serviceId, {
80+
labelType: "html",
81+
label: html,
82+
padding: 0,
83+
class: color,
6184
});
6285

63-
expectedNodes = expectedNodes.add(node);
86+
unexpectedNodes.removeObject(serviceId);
87+
});
6488

65-
var edge;
89+
unremovedServices.forEach(function(service) {
90+
var serviceId = service.get('id');
6691
(service.get('consumedservices')||[]).map(function(target) {
6792
var targetId = target.get('id');
68-
edge = cy.edges().filter(function(i, e) {
69-
return e.source().id() === serviceId && e.target().id() === targetId;
70-
})[0];
71-
72-
if ( !edge )
73-
{
74-
relayout = true;
75-
edge = cy.add({
76-
group: 'edges',
77-
data: {
78-
source: serviceId,
79-
target: targetId,
80-
},
81-
})[0];
82-
}
83-
84-
edge.data({
85-
color: (target.get('state') === 'active' ? '#41c77d' : (target.get('state') === 'inactive' ? '#f00' : '#f3bd24')),
93+
var color = (target.get('state') === 'active' ? 'green' : (target.get('state') === 'inactive' ? 'red' : 'yellow'));
94+
95+
g.setEdge(serviceId, targetId, {
96+
lineInterpolate: 'bundle',
97+
class: color,
8698
});
8799

88-
expectedEdges = expectedEdges.add(edge);
100+
var existing = unexpectedEdges.filter(function(edge) {
101+
return edge.v === serviceId && edge.w === targetId;
102+
});
103+
unexpectedEdges.removeObjects(existing);
89104
});
90105
});
91106

92-
// Remove nodes & edges that shouldn't be there
93-
cy.nodes().not(expectedNodes).remove();
94-
cy.edges().not(expectedEdges).remove();
95-
console.log('end batch');
96-
cy.endBatch();
97-
if ( relayout || startNodes !== cy.nodes().length || startEdges !== cy.edges().length )
98-
{
99-
console.log('layout');
100-
cy.layout();
101-
console.log('end layout');
102-
}
103-
console.log('done updateGraph');
104-
},
105-
106-
throttledUpdateGraph: function() {
107-
Ember.run.throttle(this,'updateGraph',500);
108-
}.observes('context.services.@each.{id,name,state,consumedServicesUpdated}'),
107+
// Remove nodes & edges that shouldn't be there anymore
108+
unexpectedNodes.forEach(function(node) {
109+
g.removeNode(node);
110+
});
109111

110-
graph: function() {
111-
var style = cytoscape.stylesheet()
112-
.selector('node')
113-
.css({
114-
'shape': 'data(shape)',
115-
'content': 'data(name)',
116-
'text-valign': 'center',
117-
'text-outline-width': 2,
118-
'text-outline-color': 'data(color)',
119-
'border-width': 2,
120-
'border-color': '#f4f5f8',
121-
'background-color': 'data(color)',
122-
'font-family': '"Open Sans"',
123-
'font-weight': 300,
124-
'font-size': 13,
125-
'color': '#fff',
126-
})
127-
.selector(':selected')
128-
.css({
129-
'border-width': 3,
130-
'border-color': '#333'
131-
})
132-
.selector('edge')
133-
.css({
134-
'opacity': 0.8,
135-
'width': 'mapData(strength, 70, 100, 2, 6)',
136-
'target-arrow-shape': 'triangle-backcurve',
137-
'source-arrow-shape': 'none',
138-
'line-color': 'data(color)',
139-
'source-arrow-color': 'data(color)',
140-
'target-arrow-color': 'data(color)'
141-
})
142-
.selector('edge.questionable')
143-
.css({
144-
'line-style': 'dotted',
145-
'target-arrow-shape': 'diamond'
146-
})
147-
.selector('.faded')
148-
.css({
149-
'opacity': 0.25,
150-
'text-opacity': 0
151-
});
152-
// End: style
153-
154-
var cy = cytoscape({
155-
container: this.get('cyElem'),
156-
style: style,
157-
userZoomingEnabled: false,
158-
userPanningEnabled: false,
159-
layout: {
160-
name: 'dagre',
161-
animate: false,
162-
padding: 20,
163-
edgeSep: 20,
164-
},
112+
unexpectedEdges.forEach(function(edge) {
113+
g.removeNode(edge.v, edge.w);
165114
});
166115

167-
this.set('cy', cy);
168-
this.updateGraph();
116+
this.renderGraph();
169117
},
170118

119+
renderGraph: function() {
120+
var zoom = this.get('graphZoom');
121+
var render = this.get('graphRender');
122+
var inner = this.get('graphInner');
123+
var outer = this.get('graphOuter');
124+
var g = this.get('graph');
125+
126+
inner.call(render, g);
127+
128+
// Zoom and scale to fit
129+
var zoomScale = zoom.scale();
130+
var graphWidth = g.graph().width;
131+
var graphHeight = g.graph().height;
132+
var width = $('#environment-graph').width();
133+
var height = $('#environment-graph').height();
134+
zoomScale = Math.min(2.0, Math.min(width / graphWidth, height / graphHeight));
135+
var translate = [(width/2) - ((graphWidth*zoomScale)/2), (height/2) - ((graphHeight*zoomScale)/2)];
136+
zoom.translate(translate);
137+
zoom.scale(zoomScale);
138+
zoom.event(outer);
139+
140+
// Overflow the foreignObjects
141+
$(this.get('graphElem').getElementsByTagName('foreignObject')).css('overflow','visible');
142+
},
143+
144+
throttledUpdateGraph: function() {
145+
Ember.run.throttle(this,'updateGraph',250);
146+
}.observes('context.services.@each.{id,name,state,consumedServicesUpdated}'),
147+
171148
willDestroyElement: function() {
172149
this._super();
173-
var cy = this.get('cy');
174-
if ( cy )
175-
{
176-
cy.destroy();
177-
}
178-
179-
var elem = this.get('cyElem');
150+
var elem = this.get('graphElem');
180151
if ( elem )
181152
{
182153
$(elem).remove();

0 commit comments

Comments
 (0)