diff --git a/.jshintrc b/.jshintrc index 2f044640a..d7a73c7f7 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,7 +10,7 @@ "Terminal", "Prism", "Ui", - "cytoscape" + "dagreD3" ], "browser" : true, "boss" : true, diff --git a/Brocfile.js b/Brocfile.js index ae4cad2f8..229ce1461 100644 --- a/Brocfile.js +++ b/Brocfile.js @@ -59,7 +59,9 @@ app.import('bower_components/bootstrap-multiselect/dist/js/bootstrap-multiselect app.import('bower_components/bootstrap-multiselect/dist/css/bootstrap-multiselect.css'); app.import('bower_components/prism/prism.js'); app.import('bower_components/prism/components/prism-yaml.js'); -app.import('bower_components/cytoscape/dist/cytoscape.js'); -app.import('bower_components/cytoscape/lib/dagre.js'); +app.import('bower_components/lodash/dist/lodash.js'); +app.import('bower_components/graphlib/dist/graphlib.core.js'); +app.import('bower_components/dagre/dist/dagre.core.js'); +app.import('bower_components/dagre-d3/dist/dagre-d3.core.js'); module.exports = app.toTree(); diff --git a/app/environment/graph/view.js b/app/environment/graph/view.js index 15ccfe908..06a5169c8 100644 --- a/app/environment/graph/view.js +++ b/app/environment/graph/view.js @@ -1,173 +1,129 @@ import Ember from 'ember'; -import ThrottledResize from 'ui/mixins/throttled-resize'; +import Util from 'ui/utils/util'; +//import ThrottledResize from 'ui/mixins/throttled-resize'; -export default Ember.View.extend(ThrottledResize,{ - cy: null, - cyElem: null, - - onResize: function() { - var cy = this.get('cy'); - if ( cy ) - { - setTimeout(function() { - cy.fit(null, 20); - },100); - } - }, +export default Ember.View.extend({ + graphZoom: null, + graphInner: null, + graphOuter: null, + graphRender: null, + graph: null, didInsertElement: function() { this._super(); - var elem = $('
').appendTo('BODY'); + var elem = $('
').appendTo('BODY'); elem.css('top', $('MAIN').position().top + $('MAIN').height() + 'px'); - this.set('cyElem', elem[0]); + this.set('graphElem', elem[0]); - Ember.run.later(this,'graph',250); + Ember.run.later(this,'initGraph',100); + }, + + initGraph: function() { + var outer = d3.select("#environment-graph svg"); + var inner = outer.select("g"); + var zoom = d3.behavior.zoom().on("zoom", function() { + // inner.attr("transform", "translate(" + d3.event.translate + ")" + + // "scale(" + d3.event.scale + ")"); + }); + + outer.call(zoom); + + var g = new dagreD3.graphlib.Graph().setGraph({ + rankdir: "TB", + nodesep: 70, + ranksep: 50, + marginx: 30, + marginy: 30 + }); + + var render = new dagreD3.render(); + this.setProperties({ + graphZoom: zoom, + graphOuter: outer, + graphInner: inner, + graphRender: render, + graph: g + }); + + this.updateGraph(); }, updateGraph: function() { - console.log('starting updateGraph'); - var cy = this.get('cy'); - cy.startBatch(); - + var g = this.get('graph'); var services = this.get('context.services'); - - var relayout = false; - var startNodes = cy.nodes().length; - var startEdges = cy.edges().length; - var expectedNodes = cy.collection(); - var expectedEdges = cy.collection(); var unremovedServices = services.filter(function(service) { return ['removed','purging','purged'].indexOf(service.get('state')) === -1; }); unremovedServices.forEach(function(service) { var serviceId = service.get('id'); - var node = cy.getElementById(serviceId)[0]; - if ( !node ) - { - relayout = true; - node = cy.add({ - group: 'nodes', - data: { - id: serviceId, - shape: 'ellipse', - }, - position: {x: 0, y: 0} - })[0]; - } - node.data({ - name: service.get('name'), - color: (service.get('state') === 'active' ? '#41c77d' : (service.get('state') === 'inactive' ? '#f00' : '#f3bd24')), + var html = '

'+ Util.escapeHtml(service.get('name')) + '

' + Util.escapeHtml(Util.ucFirst(service.get('state'))) + '
'; + + g.setNode(serviceId, { + labelType: "html", + label: html, + padding: 0, + class: (service.get('state') === 'active' ? 'green' : (service.get('state') === 'inactive' ? 'red' : 'yellow')), }); + }); - expectedNodes = expectedNodes.add(node); - - var edge; + unremovedServices.forEach(function(service) { + var serviceId = service.get('id'); (service.get('consumedservices')||[]).map(function(target) { var targetId = target.get('id'); - edge = cy.edges().filter(function(i, e) { - return e.source().id() === serviceId && e.target().id() === targetId; - })[0]; - - if ( !edge ) + var color = '#f3bd24'; + if ( target.get('state') === 'active' ) { - relayout = true; - edge = cy.add({ - group: 'edges', - data: { - source: serviceId, - target: targetId, - }, - })[0]; + color = '#41c77d'; + } + else if (target.get('state') === 'inactive' ) + { + color = '#ff0000'; } - edge.data({ - color: (target.get('state') === 'active' ? '#41c77d' : (target.get('state') === 'inactive' ? '#f00' : '#f3bd24')), + g.setEdge(serviceId, targetId, { + label: '', + arrowhead: 'vee', + lineInterpolate: 'bundle', + class: (target.get('state') === 'active' ? 'green' : (target.get('state') === 'inactive' ? 'red' : 'yellow')), }); - - expectedEdges = expectedEdges.add(edge); }); }); // Remove nodes & edges that shouldn't be there - cy.nodes().not(expectedNodes).remove(); - cy.edges().not(expectedEdges).remove(); - console.log('end batch'); - cy.endBatch(); - if ( relayout || startNodes !== cy.nodes().length || startEdges !== cy.edges().length ) - { - console.log('layout'); - cy.layout(); - console.log('end layout'); - } + + this.renderGraph(); console.log('done updateGraph'); }, + renderGraph: function() { + console.log('start renderGraph'); + var zoom = this.get('graphZoom'); + var render = this.get('graphRender'); + var inner = this.get('graphInner'); + var outer = this.get('graphOuter'); + var g = this.get('graph'); + + inner.call(render, g); + + // Zoom and scale to fit + var zoomScale = zoom.scale(); + var graphWidth = g.graph().width + 80; + var graphHeight = g.graph().height + 40; + var width = $('#environment-graph').width(); + var height = $('#environment-graph').height(); + zoomScale = Math.min(width / graphWidth, height / graphHeight); + var translate = [(width/2) - ((graphWidth*zoomScale)/2), (height/2) - ((graphHeight*zoomScale)/2)]; + zoom.translate(translate); + zoom.scale(zoomScale); + zoom.event(outer.transition().duration(500)); + console.log('done renderGraph'); + }, + throttledUpdateGraph: function() { Ember.run.throttle(this,'updateGraph',500); }.observes('context.services.@each.{id,name,state,consumedServicesUpdated}'), - graph: function() { - var style = cytoscape.stylesheet() - .selector('node') - .css({ - 'shape': 'data(shape)', - 'content': 'data(name)', - 'text-valign': 'center', - 'text-outline-width': 2, - 'text-outline-color': 'data(color)', - 'border-width': 2, - 'border-color': '#f4f5f8', - 'background-color': 'data(color)', - 'font-family': '"Open Sans"', - 'font-weight': 300, - 'font-size': 13, - 'color': '#fff', - }) - .selector(':selected') - .css({ - 'border-width': 3, - 'border-color': '#333' - }) - .selector('edge') - .css({ - 'opacity': 0.8, - 'width': 'mapData(strength, 70, 100, 2, 6)', - 'target-arrow-shape': 'triangle-backcurve', - 'source-arrow-shape': 'none', - 'line-color': 'data(color)', - 'source-arrow-color': 'data(color)', - 'target-arrow-color': 'data(color)' - }) - .selector('edge.questionable') - .css({ - 'line-style': 'dotted', - 'target-arrow-shape': 'diamond' - }) - .selector('.faded') - .css({ - 'opacity': 0.25, - 'text-opacity': 0 - }); - // End: style - - var cy = cytoscape({ - container: this.get('cyElem'), - style: style, - userZoomingEnabled: false, - userPanningEnabled: false, - layout: { - name: 'dagre', - animate: false, - padding: 20, - edgeSep: 20, - }, - }); - - this.set('cy', cy); - this.updateGraph(); - }, - willDestroyElement: function() { this._super(); var cy = this.get('cy'); @@ -176,7 +132,7 @@ export default Ember.View.extend(ThrottledResize,{ cy.destroy(); } - var elem = this.get('cyElem'); + var elem = this.get('graphElem'); if ( elem ) { $(elem).remove(); diff --git a/app/styles/environment.scss b/app/styles/environment.scss index 29ed34f23..575f2dbc3 100644 --- a/app/styles/environment.scss +++ b/app/styles/environment.scss @@ -6,4 +6,64 @@ bottom: 0px; z-index: 3; overflow: hidden; + + .node { + &.green rect { + stroke: #41c77d; + } + + &.yellow rect { + stroke: #f3bd24; + } + + &.red rect { + stroke: #ff0000; + } + + g div { + width: 100px; + height: 40px; + padding: 10px 5px; + } + + .label { + color: black; + display: initial; + padding: initial; + font-size: initial; + font-weight: initial; + line-height: initial; + color: initial; + text-align: center; + white-space: initial; + vertical-align: initial; + border-radius: initial; + } + + rect { + fill: #fff; + stroke-width: 3px; + color: black; + } + } + + .edgePath { + path.path { + stroke: #333; + fill: none; + stroke-width: 3px; + } + + &.green path.path { + stroke: #41c77d; + } + + &.yellow path.path { + stroke: #f3bd24; + } + + &.red path.path { + stroke: #ff0000; + } + } } diff --git a/bower.json b/bower.json index 405bcc272..1809e7e93 100644 --- a/bower.json +++ b/bower.json @@ -20,7 +20,7 @@ "bootstrap-multiselect": "~0.9.10", "zeroclipboard": "~2.2.0", "prism": "gh-pages", - "cytoscape": "~2.3.15", - "dagre": "~0.7.1" + "dagre": "~0.7.1", + "dagre-d3": "~0.4.3" } }