| |
| /* |
| * |
| * Copyright 2016 RIFT.IO Inc |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| */ |
| import React from 'react'; |
| import ReactDOM from 'react-dom'; |
| import d3 from 'd3'; |
| import DashboardCard from '../dashboard_card/dashboard_card.jsx'; |
| import _ from 'lodash'; |
| import $ from 'jquery'; |
| import './topologyTree.scss'; |
| |
| export default class TopologyTree extends React.Component { |
| constructor(props) { |
| super(props); |
| this.data = props.data; |
| this.selectedID = 0; |
| this.nodeCount = 0; |
| this.size = this.wrapperSize(); |
| this.tree = d3.layout.tree() |
| .size([this.props.treeHeight, this.props.treeWidth]); |
| this.diagonal = d3.svg.diagonal() |
| .projection(function(d) { return [d.y, d.x]; }); |
| this.svg = null; |
| } |
| componentWillReceiveProps(props) { |
| let self = this; |
| if(!this.svg) { |
| let zoom = d3.behavior.zoom() |
| .translate([this.props.maxLabel, 0]) |
| .scaleExtent([this.props.minScale, this.props.maxScale]) |
| .on("zoom", self.zoom); |
| let svg = this.selectParent().append("svg") |
| .attr("width", this.size.width) |
| .attr("height", this.size.height) |
| .append("g") |
| .call(zoom) |
| .append("g") |
| .attr("transform", "translate(" + this.props.maxLabel + ",0)"); |
| |
| svg.append("rect") |
| .attr("class", "overlay") |
| .attr("width", this.size.width) |
| .attr("height", this.size.height); |
| // this.svg = d3.select() |
| this.svg = svg; |
| this.props.selectNode(props.data); |
| } |
| if(props.data.hasOwnProperty('type') && !this.props.hasSelected) { |
| this.selectedID = props.data.id; |
| //Commenting out to prevent transmitter push error |
| //this.props.selectNode(props.data); |
| } |
| if(this.svg) { |
| this.update(_.cloneDeep(props.data)); |
| // this.selectedID = props.data.id; |
| } |
| } |
| |
| wrapperSize() { |
| if (this.props.useDynamicWrapperSize) { |
| try { |
| let wrapper = $(".topologyTreeGraph-body"); |
| |
| return { |
| width: wrapper.width(), |
| height: wrapper.height() |
| } |
| } catch (e) { |
| console.log("ERROR: cannot get width and/or height from element."+ |
| " Using props for width and height. e=", e); |
| return { |
| width: this.props.width, |
| height: this.props.height |
| } |
| } |
| } else { |
| return { |
| width: this.props.width, |
| height: this.props.height |
| } |
| } |
| } |
| selectParent() { |
| return d3.select(document.querySelector('#topology')); |
| } |
| computeRadius(d) { |
| // if(d.parameters && d.parameters.vcpu) { |
| // return this.props.radius + d.parameters.vcpu.total; |
| // } else { |
| return this.props.radius; |
| // } |
| } |
| click = (d) => { |
| this.props.selectNode(d); |
| this.selectedID = d.id; |
| // if (d.children){ |
| // d._children = d.children; |
| // d.children = null; |
| // } |
| // else{ |
| // d.children = d._children; |
| // d._children = null; |
| // } |
| // this.update(d); |
| } |
| zoom = () => { |
| this.svg.attr("transform", "translate(" + d3.event.translate + |
| ")scale(" + d3.event.scale + ")"); |
| } |
| update = (source) => { |
| // Compute the new tree layout. |
| var svg = this.svg; |
| var nodes = this.tree.nodes(source).reverse(); |
| var links = this.tree.links(nodes); |
| var self = this; |
| |
| // Normalize for fixed-depth. |
| nodes.forEach(function(d) { d.y = d.depth * self.props.maxLabel; }); |
| // Update the nodes… |
| var node = svg.selectAll("g.node") |
| .data(nodes, function(d){ |
| return d.id || (d.id = ++self.nodeCount); |
| }); |
| // Enter any new nodes at the parent's previous position. |
| var nodeEnter = node.enter() |
| .append("g") |
| .attr("class", "node") |
| .attr("transform", function(d){ |
| return "translate(" + source.y0 + "," + source.x0 + ")"; }) |
| .on("click", this.click); |
| |
| nodeEnter.append("circle") |
| .attr("r", 0) |
| .style("fill", function(d){ |
| return d._children ? "lightsteelblue" : "white"; |
| }); |
| |
| nodeEnter.append("text") |
| .attr("x", function(d){ |
| var spacing = self.computeRadius(d) + 5; |
| return d.children || d._children ? -spacing : spacing; }) |
| .attr("transform", function(d, i) { |
| return "translate(0," + ((i%2) ? 15 : -15) + ")"; }) |
| .attr("dy", "3") |
| .attr("text-anchor", function(d){ |
| return d.children || d._children ? "end" : "start"; |
| }) |
| .text(function(d){ return d.name; }) |
| .style("fill-opacity", 0); |
| |
| // Transition nodes to their new position. |
| var nodeUpdate = node |
| .transition() |
| .duration(this.props.duration) |
| .attr("transform", function(d) { |
| return "translate(" + d.y + "," + d.x + ")"; }); |
| |
| nodeUpdate.select("circle") |
| .attr("r", function(d){ return self.computeRadius(d); }) |
| .style("fill", function(d) { |
| return d.id == self.selectedID ? "green" : "lightsteelblue"; }); |
| |
| nodeUpdate.select("text") |
| .style("fill-opacity", 1) |
| .style("font-weight", function(d) { |
| return d.id == self.selectedID ? "900" : "inherit"; |
| }) |
| .attr("transform", function(d, i) { |
| return "translate(0," + ((i%2) ? 15 : -15) + ")" + |
| (d.id == self.selectedID ? "scale(1.125)" : "scale(1)"); |
| }); |
| |
| // Transition exiting nodes to the parent's new position. |
| var nodeExit = node.exit() |
| .transition() |
| .duration(this.props.duration) |
| .attr("transform", function(d) { |
| return "translate(" + source.y + "," + source.x + ")"; }) |
| .remove(); |
| |
| nodeExit.select("circle").attr("r", 0); |
| nodeExit.select("text").style("fill-opacity", 0); |
| |
| // Update the links… |
| var link = svg.selectAll("path.link") |
| .data(links, function(d){ return d.target.id; }); |
| |
| // Enter any new links at the parent's previous position. |
| link.enter().insert("path", "g") |
| .attr("class", "link") |
| .attr("d", function(d){ |
| var o = {x: source.x0, y: source.y0}; |
| return self.diagonal({source: o, target: o}); |
| }); |
| |
| // Transition links to their new position. |
| link |
| .transition() |
| .duration(this.props.duration) |
| .attr("d", self.diagonal); |
| |
| // Transition exiting nodes to the parent's new position. |
| link.exit() |
| .transition() |
| .duration(this.props.duration) |
| .attr("d", function(d){ |
| var o = {x: source.x, y: source.y}; |
| return self.diagonal({source: o, target: o}); |
| }) |
| .remove(); |
| |
| // Stash the old positions for transition. |
| nodes.forEach(function(d){ |
| d.x0 = d.x; |
| d.y0 = d.y; |
| }); |
| } |
| render() { |
| let html = ( |
| <DashboardCard className="topologyTreeGraph" showHeader={true} title="Topology Tree" |
| headerExtras={this.props.headerExtras} > |
| <div id="topology"></div> |
| </DashboardCard> |
| ); |
| return html; |
| } |
| } |
| |
| TopologyTree.defaultProps = { |
| treeWidth: 800, |
| treeHeight: 500, |
| width: 800, |
| height: 800, |
| minScale: 0.5, |
| maxScale: 2, |
| maxLabel: 150, |
| duration: 500, |
| radius: 5, |
| useDynamicWrapperSize: false, |
| data: {} |
| } |