http://git-wip-us.apache.org/repos/asf/ambari/blob/7e00f585/contrib/views/storm/src/main/resources/scripts/components/BarChart.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/components/BarChart.jsx b/contrib/views/storm/src/main/resources/scripts/components/BarChart.jsx index 7dabed1..ef05422 100644 --- a/contrib/views/storm/src/main/resources/scripts/components/BarChart.jsx +++ b/contrib/views/storm/src/main/resources/scripts/components/BarChart.jsx @@ -55,7 +55,7 @@ define(['react', 'react-dom', 'd3', 'd3.tip'], function(React, ReactDOM, d3) { setUpSVG: function(){ this.svg = d3.select(ReactDOM.findDOMNode(this)) .attr('width', this.props.width+"px") - .attr('height', this.props.height+30+"px") + .attr('height', this.props.height+50+"px") // .attr("viewBox", "-46 -5 " + (this.props.width+82) + " " + (this.props.height+28) ); this.container = this.svg.append("g") @@ -136,7 +136,37 @@ define(['react', 'react-dom', 'd3', 'd3.tip'], function(React, ReactDOM, d3) { this.xAxisGrp = container['xAxisEl'] = container.append("g") .attr("class", "x axis") .attr("transform", "translate(0," + height + ")") - .call(xAxis); + .call(xAxis) + .selectAll(".tick text") + .call(this.wrap, this.x.rangeBand()); + }, + wrap: function(text, width) { + text.each(function() { + var text = d3.select(this), + words = text.text().split(/-+/).reverse(), + word, + line = [], + lineNumber = 0, + lineHeight = 1.1, // ems + y = text.attr("y"), + dy = parseFloat(text.attr("dy")), + tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em"); + + //Hack to show hidden div to find getComputedTextLength + $('#lag-graph').css({visibility: 'hidden', display: 'block', position: 'absolute'}); + + while (word = words.pop()) { + line.push(word); + tspan.text(line.join(" ")); + if (tspan.node().getComputedTextLength() > width) { + line.pop(); + tspan.text(line.join(" ")); + line = [word]; + tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word); + } + } + $('#lag-graph').css({visibility: '', display: 'none', position: ''}); + }); }, drawYAxis: function(x) { var yAxis = this.yAxis;
http://git-wip-us.apache.org/repos/asf/ambari/blob/7e00f585/contrib/views/storm/src/main/resources/scripts/components/TopologyGraph.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/components/TopologyGraph.jsx b/contrib/views/storm/src/main/resources/scripts/components/TopologyGraph.jsx index 44afd5d..8679661 100644 --- a/contrib/views/storm/src/main/resources/scripts/components/TopologyGraph.jsx +++ b/contrib/views/storm/src/main/resources/scripts/components/TopologyGraph.jsx @@ -16,7 +16,7 @@ limitations under the License. */ -define(['react', 'react-dom', 'd3', 'd3.tip'], function(React, ReactDOM, d3) { +define(['react', 'react-dom', 'd3', 'dagreD3', 'd3.tip'], function(React, ReactDOM, d3, dagreD3) { 'use strict'; return React.createClass({ displayName: 'TopologyGraph', @@ -26,39 +26,24 @@ define(['react', 'react-dom', 'd3', 'd3.tip'], function(React, ReactDOM, d3) { height: React.PropTypes.string }, getInitialState: function(){ - this.width = this.props.width || '1100'; - this.height = this.props.height || '260'; this.syncData(this.props.data); return null; }, - componentWillUpdate: function(){ - - }, componentDidUpdate: function(){ this.syncData(this.props.data); this.updateGraph(); - this.force.start(); - for (var i = 300; i > 0; --i) this.force.tick(); - this.force.stop(); }, componentDidMount: function(){ - var width = this.width, - height = this.height, - nodes = this.nodeArray, - links = this.linkArray, - radius = this.radius = 20.75; - - var svg = this.svg = d3.select(ReactDOM.findDOMNode(this)) - .attr('width', width) - .attr('height', height); - + var that = this; + this.svg = d3.select(ReactDOM.findDOMNode(this)) //Set up tooltip - this.tip = d3.tip() + this.tooltip = d3.tip() .attr('class', function() { - return 'd3-tip'; + return 'd3-tip testing'; }) .offset([-10, 0]) - .html(function(d) { + .html(function(data) { + var d = that.g.node(data); var tip = "<ul>"; if (d[":capacity"] !== null) tip += "<li>Capacity: " + d[":capacity"].toFixed(2) + "</li>"; if (d[":latency"] !== null) tip += "<li>Latency: " + d[":latency"].toFixed(2) + "</li>"; @@ -66,171 +51,105 @@ define(['react', 'react-dom', 'd3', 'd3.tip'], function(React, ReactDOM, d3) { tip += "</ul>"; return tip; }); - svg.call(this.tip); - - // define arrow markers for graph links - svg.append('svg:defs').append('svg:marker') - .attr('id', 'end-arrow') - .attr('viewBox', '0 -5 10 10') - .attr('refX', 6) - .attr('markerWidth', 6) - .attr('markerHeight', 6.5) - .attr('orient', 'auto') - .append('svg:path') - .attr('d', 'M0,-5L10,0L0,5'); - - svg.append('svg:defs').append('svg:marker') - .attr('id', 'start-arrow') - .attr('viewBox', '0 -5 10 10') - .attr('refX', 4) - .attr('markerWidth', 3) - .attr('markerHeight', 3) - .attr('orient', 'auto') - .append('svg:path') - .attr('d', 'M10,-5L0,0L10,5'); - - // handles to link and node element groups - this.path = svg.append('svg:g').selectAll('path'); - this.image = svg.append('svg:g').selectAll('g'); - - this.selected_node = null; - - // only respond once per keydown - this.lastKeyDown = -1; - d3.select(window) - .on('keydown', this.keydown) - .on('keyup', this.keyup); - this.updateGraph(); - this.force.start(); - this.force.tick(); - this.force.stop(); - }, + //Set up zoom + this.zoom = d3.behavior.zoom() + .scaleExtent([0, 8]) + .on("zoom", this.zoomed); + }, + zoomed: function(){ + this.inner.attr("transform", + "translate(" + this.zoom.translate() + ")" + + "scale(" + this.zoom.scale() + ")" + ); + }, // update graph (called when needed) updateGraph: function(){ - // init D3 force layout - this.force = d3.layout.force() - .nodes(this.nodeArray) - .links(this.linkArray) - .size([this.width, this.height]) - .linkDistance(150) - .charge(-500) - .on('tick', this.tick); - - // path (link) group - this.path = this.path.data(this.linkArray); - - // update existing links - this.path.style('marker-start', function(d) { - return ''; }) - .style('marker-end', function(d) { - return 'url(#end-arrow)'; }); - - - // add new links - this.path.enter().append('svg:path') - .attr('class', 'link') - .style('marker-start', function(d) { - return ''; }) - .attr("stroke-dasharray", "5, 5") - .attr('stroke-width', '2') - .style('marker-end', function(d) { - return 'url(#end-arrow)'; }); - - // remove old links - this.path.exit().remove(); - - - // image (node) group - // NB: the function arg is crucial here! nodes are known by id, not by index! - this.image = this.image.data(this.nodeArray, function(d) { - return d.id; - }); - - //update old nodes - this.image - .on('mouseover', function(d) { - this.tip.show(d); - }.bind(this)) - .on('mouseout', function(d) { - this.tip.hide(); - }.bind(this)); - - // add new nodes - var g = this.image.enter().append('svg:g'); + var that = this; + var g = ReactDOM.findDOMNode(this).children[0]; + if(g){ + g.remove(); + } + var inner = this.inner = this.svg.append("g"); + // Create the renderer + var render = new dagreD3.render(); + + render.arrows().arrowPoint = function normal(parent, id, edge, type) { + var marker = parent.append("marker") + .attr("id", id) + .attr("viewBox", "0 0 10 10") + .attr("refX", 5) + .attr("refY", 5) + .attr("markerUnits", "strokeWidth") + .attr("markerWidth", 6) + .attr("markerHeight", 6.5) + .attr("orient", "auto"); + + var path = marker.append("path") + .attr("d", "M 0 0 L 10 5 L 0 10 z") + .style("stroke-width", 1) + .style("stroke-dasharray", "1,0") + .style("fill", "grey") + .style("stroke", "grey"); + dagreD3.util.applyStyle(path, edge[type + "Style"]); + }; + + render.shapes().img = function circle(parent, bbox, node) { + var shapeSvg = parent.insert("image") + .attr("class", "nodeImage") + .attr("xlink:href", function(d) { + if (node) { + if(node.type === 'spout'){ + return "images/icon-spout.png"; + } else if(node.type === 'bolt'){ + return "images/icon-bolt.png"; + } + } + }).attr("x", "-12px") + .attr("y", "-12px") + .attr("width", "30px") + .attr("height", "30px"); + node.intersect = function(point) { + return dagreD3.intersect.circle(node, 20, point); + }; + return shapeSvg; + } + this.svg.call(this.zoom).call(this.tooltip); + // Run the renderer. This is what draws the final graph. + render(inner, this.g); - g.append('svg:image') - .attr("xlink:href", function(d){ - if(d.type === 'spout'){ - return "images/icon-spout.png"; - } else if(d.type === 'bolt'){ - return "images/icon-bolt.png"; - } - }) - .attr("width", "30px") - .attr("height", "30px") + inner.selectAll("g.nodes image") .on('mouseover', function(d) { - this.tip.show(d); - }.bind(this)) + that.tooltip.show(d); + }) .on('mouseout', function(d) { - this.tip.hide(); - }.bind(this)); - - g.append("svg:text") - .attr("dx", 18) - .attr("dy", 38) - .text(function(d) { - return d.id; }); - - // remove old nodes - this.image.exit().remove(); - }, - // update force layout (called automatically each iteration) - tick: function(){ - // draw directed edges with proper padding from node centers - this.path.attr('d', function(d) { - var deltaX = d.target.x - d.source.x, - deltaY = d.target.y - d.source.y, - dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY), - normX = deltaX / dist, - normY = deltaY / dist, - sourcePadding = 30, - targetPadding = 5, - sourceX = d.source.x + (sourcePadding * normX), - sourceY = d.source.y + (sourcePadding * normY) + 15, - targetX = d.target.x - (targetPadding * normX), - targetY = d.target.y - (targetPadding * normY) + 15; - return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY; - }); - - this.image.attr('transform', function(d) { - return 'translate(' + Math.max(this.radius, Math.min(this.width - this.radius, d.x)) + ',' + Math.max(this.radius, Math.min(this.height - this.radius, d.y)) + ')'; - }.bind(this)); - }, - keydown: function(){ - if (this.lastKeyDown !== -1) return; - this.lastKeyDown = d3.event.keyCode; + that.tooltip.hide(); + }); - // ctrl - if (d3.event.keyCode === 17) { - d3.event.preventDefault(); - this.tip.hide(); - this.image.call(this.force.drag); - this.svg.classed('ctrl', true); + inner.selectAll("g.nodes g.label") + .attr("transform", "translate(2,-30)"); + // Center the graph + var initialScale = 1; + var svgWidth = this.svg[0][0].parentNode.clientWidth; + var svgHeight = this.svg[0][0].parentNode.clientHeight; + if(this.linkArray.length > 0){ + this.zoom.translate([(svgWidth - this.g.graph().width * initialScale) / 2, (svgHeight - this.g.graph().height * initialScale) / 2]) + .scale(initialScale) + .event(this.svg); } - }, - keyup: function(){ - this.lastKeyDown = -1; - // ctrl - if (d3.event.keyCode === 17) { - this.image - .on('mousedown.drag', null) - .on('touchstart.drag', null); - this.svg.classed('ctrl', false); - } - }, + }, syncData: function(data){ this.nodeArray = []; this.linkArray = []; + this.g = new dagreD3.graphlib.Graph().setGraph({ + nodesep: 50, + ranksep: 190, + rankdir: "LR", + marginx: 20, + marginy: 20, + // transition: function transition(selection) { + // return selection.transition().duration(500); + // } + }); if(data){ var keys = _.keys(data); keys.map(function(key){ @@ -243,10 +162,11 @@ define(['react', 'react-dom', 'd3', 'd3.tip'], function(React, ReactDOM, d3) { var spoutObjArr = _.where(this.nodeArray, { "type": "spout" }); if (spoutObjArr.length > 1) { - var index = this.nodeArray.length - 1; - this.nodeArray[index].x = 50; - this.nodeArray[index].y = 100; - this.nodeArray[index].fixed = true; + for(var i = 0; i < spoutObjArr.length; i++){ + spoutObjArr[i].x = 50; + spoutObjArr[i].y = parseInt(i+'10', 10); + spoutObjArr[i].fixed = true; + } } else if (spoutObjArr.length == 1) { spoutObjArr[0].x = 50; spoutObjArr[0].y = 100; @@ -257,10 +177,14 @@ define(['react', 'react-dom', 'd3', 'd3.tip'], function(React, ReactDOM, d3) { var inputArr = node[":inputs"] || []; inputArr.map(function(input){ if(!input[":component"].startsWith("__")){ - this.linkArray.push({ - source: _.findWhere(this.nodeArray, {id: input[":component"]}), + var sourceNode = _.findWhere(this.nodeArray, {id: input[":component"]}); + this.linkArray.push({ + source: sourceNode, target: node }); + this.g.setNode(sourceNode.id, _.extend(sourceNode, {label: sourceNode.id, shape: 'img'})); + this.g.setNode(node.id, _.extend(node, {label: node.id, shape: 'img'})); + this.g.setEdge(sourceNode.id, node.id, {"arrowhead": 'arrowPoint'}); } }.bind(this)); }.bind(this)); @@ -268,7 +192,7 @@ define(['react', 'react-dom', 'd3', 'd3.tip'], function(React, ReactDOM, d3) { }, render: function() { return ( - <svg className="topology-graph"></svg> + <svg className="topology-graph" width="100%" height="300"></svg> ); }, }); http://git-wip-us.apache.org/repos/asf/ambari/blob/7e00f585/contrib/views/storm/src/main/resources/scripts/main.js ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/main.js b/contrib/views/storm/src/main/resources/scripts/main.js index 6b8fe95..7bd2201 100644 --- a/contrib/views/storm/src/main/resources/scripts/main.js +++ b/contrib/views/storm/src/main/resources/scripts/main.js @@ -49,6 +49,10 @@ require.config({ 'd3.tip': { deps: ['d3'] }, + 'dagreD3':{ + deps: ['d3'], + exports: 'dagreD3' + }, 'x-editable': { deps: ['bootstrap'] } @@ -70,7 +74,8 @@ require.config({ 'react-dom': '../libs/react/js/react-dom', 'JSXTransformer': '../libs/jsx/JSXTransformer', 'jsx': "../libs/jsx/jsx", - 'x-editable':'../libs/Bootstrap/js/bootstrap-editable.min' + 'x-editable':'../libs/Bootstrap/js/bootstrap-editable.min', + 'dagreD3': '../libs/dagre-d3/dagre-d3.min' }, jsx: { fileExtension: '.jsx', http://git-wip-us.apache.org/repos/asf/ambari/blob/7e00f585/contrib/views/storm/src/main/resources/scripts/views/TopologyDetailView.jsx ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/scripts/views/TopologyDetailView.jsx b/contrib/views/storm/src/main/resources/scripts/views/TopologyDetailView.jsx index 24d3271..136d95f 100644 --- a/contrib/views/storm/src/main/resources/scripts/views/TopologyDetailView.jsx +++ b/contrib/views/storm/src/main/resources/scripts/views/TopologyDetailView.jsx @@ -50,7 +50,6 @@ define([ this.lagCollection = new BaseCollection(); this.systemFlag = false; this.windowSize = ':all-time'; - this.initializeData(); return { model: this.model, graphData: {}, @@ -63,6 +62,7 @@ define([ }, componentWillMount: function(){ $('.loader').show(); + this.initializeData(); }, componentDidMount: function(){ $(".boot-switch.systemSum").bootstrapSwitch({ @@ -89,7 +89,6 @@ define([ $('.loader').hide(); }, componentWillUpdate: function(){ - $('.loader').show(); $('#collapse-spout').off('hidden.bs.collapse'); $('#collapse-spout').off('shown.bs.collapse'); $('#collapse-bolt').off('hidden.bs.collapse'); @@ -122,7 +121,6 @@ define([ if(this.refs.barChart){ ReactDOM.findDOMNode(document.getElementById('lag-graph')).appendChild(this.refs.barChart.legendsEl) } - $('.loader').hide(); }, initializeData: function(){ this.model.getData({ @@ -147,6 +145,7 @@ define([ this.initializeWorkerData(); }, initializeGraphData: function(){ + $('#graphLoader').show(); this.model.getGraphData({ id: this.model.get('id'), window: this.windowSize, @@ -159,6 +158,7 @@ define([ } this.setState({graphData: model}); } + $('#graphLoader').hide(); }.bind(this), error: function(model, response, options){ Utils.notifyError("Error occured in fetching topology visualization data."); @@ -184,23 +184,40 @@ define([ }, initializeLagData: function(){ + $('#kafkaLoader').show(); this.model.getTopologyLag({ id: this.model.get('id'), success: function(model, response){ if(response.error || model.error){ Utils.notifyError(response.error || model.error+'('+model.errorMessage.split('(')[0]+')'); } else { - if(model && model.length){ - var result = JSON.parse(model[0].spoutLagResult); - for(var i = 0; i < result.length; i++){ - result[i]['spoutId'] = model[0].spoutId; - result[i]['spoutType'] = model[0].spoutType; + if(model && _.keys(model).length > 0){ + var keys = _.keys(model); + var arr = []; + for(var i = 0; i < keys.length; i++){ + var data = model[keys[i]]; + var topicKeys = _.keys(data.spoutLagResult); + for(var j = 0; j < topicKeys.length; j++){ + var topicName = topicKeys[j]; + var partitionData = data.spoutLagResult[topicName]; + var id = _.keys(partitionData); + for(var k = 0; k < id.length; k++){ + var partitionId = id[k]; + var obj = partitionData[partitionId]; + obj['spoutId'] = data.spoutId; + obj['spoutType'] = data.spoutType; + obj['partition'] = partitionId; + obj['topic'] = topicName; + arr.push(obj); + } + } } - this.resetLagCollection(result); + this.resetLagCollection(arr); } else { this.setState({hideKafkaLagBox : true}); } } + $('#kafkaLoader').hide(); }.bind(this) }) }, @@ -788,6 +805,7 @@ define([ </div> <div className="row"> <div className="col-sm-12"> + <div className="inner-loader" id="graphLoader" /> <TopologyDetailGraph model={this.state.model} graphData={this.state.graphData}/> </div> </div> @@ -811,7 +829,8 @@ define([ <div className="box-body"> <div className="row"> <div className="col-sm-12"> - <div id="lag-graph" className="displayNone"> + <div className="inner-loader" id="kafkaLoader" /> + <div id="lag-graph"> {this.lagCollection.length > 0 ? <BarChart ref="barChart" http://git-wip-us.apache.org/repos/asf/ambari/blob/7e00f585/contrib/views/storm/src/main/resources/styles/style.css ---------------------------------------------------------------------- diff --git a/contrib/views/storm/src/main/resources/styles/style.css b/contrib/views/storm/src/main/resources/styles/style.css index 4c51233..f6b1685 100644 --- a/contrib/views/storm/src/main/resources/styles/style.css +++ b/contrib/views/storm/src/main/resources/styles/style.css @@ -425,6 +425,18 @@ REACT position: absolute; text-align: center; } +.node ellipse { + stroke: #333; + fill: #fff; + stroke-width: 1px; +} +.edgePath path { + stroke: grey; + fill: none; + cursor: default; + stroke-width: 2; + stroke-dasharray: 5, 5; +} /* Style northward tooltips differently */ .d3-tip.n:after { @@ -533,6 +545,14 @@ ul.legends li.legend{ background: url('../images/loader.gif') rgba(255,255,255,0.75) no-repeat center center; z-index: 9; } +.inner-loader{ + position: absolute; + top: 0px; + left: 0px; + bottom: 0px; + right: 0px; + background: url('../images/loader.gif') rgba(255,255,255,0.75) no-repeat center center; +} .searchbar{ margin-top: 15px; }