/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { Component, } = require("resource://devtools/client/shared/vendor/react.js"); const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); const { isSavedFrame } = require("resource://devtools/shared/DevToolsUtils.js"); const { getSourceNames, } = require("resource://devtools/client/shared/source-utils.js"); const { L10N } = require("resource://devtools/client/memory/utils.js"); const GRAPH_DEFAULTS = { translate: [20, 20], scale: 1, }; const NO_STACK = "noStack"; const NO_FILENAME = "noFilename"; const ROOT_LIST = "JS::ubi::RootList"; function stringifyLabel(label, id) { const sanitized = []; for (let i = 0, length = label.length; i < length; i++) { const piece = label[i]; if (isSavedFrame(piece)) { const { short } = getSourceNames(piece.source); sanitized[i] = `${piece.functionDisplayName} @ ` + `${short}:${piece.line}:${piece.column}`; } else if (piece === NO_STACK) { sanitized[i] = L10N.getStr("tree-item.nostack"); } else if (piece === NO_FILENAME) { sanitized[i] = L10N.getStr("tree-item.nofilename"); } else if (piece === ROOT_LIST) { // Don't use the usual labeling machinery for root lists: replace it // with the "GC Roots" string. sanitized.splice(0, label.length); sanitized.push(L10N.getStr("tree-item.rootlist")); break; } else { sanitized[i] = "" + piece; } } return `${sanitized.join(" › ")} @ 0x${id.toString(16)}`; } class ShortestPaths extends Component { static get propTypes() { return { graph: PropTypes.shape({ nodes: PropTypes.arrayOf(PropTypes.object), edges: PropTypes.arrayOf(PropTypes.object), }), }; } constructor(props) { super(props); this.state = { zoom: null }; this._renderGraph = this._renderGraph.bind(this); } componentDidMount() { if (this.props.graph) { this._renderGraph(this.refs.container, this.props.graph); } } shouldComponentUpdate(nextProps) { return this.props.graph != nextProps.graph; } componentDidUpdate() { if (this.props.graph) { this._renderGraph(this.refs.container, this.props.graph); } } componentWillUnmount() { if (this.state.zoom) { this.state.zoom.on("zoom", null); } } _renderGraph(container, { nodes, edges }) { if (!container.firstChild) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("id", "graph-svg"); svg.setAttribute("xlink", "http://www.w3.org/1999/xlink"); svg.style.width = "100%"; svg.style.height = "100%"; const target = document.createElementNS( "http://www.w3.org/2000/svg", "g" ); target.setAttribute("id", "graph-target"); target.style.width = "100%"; target.style.height = "100%"; svg.appendChild(target); container.appendChild(svg); } const graph = new dagreD3.Digraph(); for (let i = 0; i < nodes.length; i++) { graph.addNode(nodes[i].id, { id: nodes[i].id, label: stringifyLabel(nodes[i].label, nodes[i].id), }); } for (let i = 0; i < edges.length; i++) { graph.addEdge(null, edges[i].from, edges[i].to, { label: edges[i].name, }); } const renderer = new dagreD3.Renderer(); renderer.drawNodes(); renderer.drawEdgePaths(); const svg = d3.select("#graph-svg"); const target = d3.select("#graph-target"); let zoom = this.state.zoom; if (!zoom) { zoom = d3.behavior.zoom().on("zoom", function () { target.attr( "transform", `translate(${d3.event.translate}) scale(${d3.event.scale})` ); }); svg.call(zoom); this.setState({ zoom }); } const { translate, scale } = GRAPH_DEFAULTS; zoom.scale(scale); zoom.translate(translate); target.attr("transform", `translate(${translate}) scale(${scale})`); const layout = dagreD3.layout(); renderer.layout(layout).run(graph, target); } render() { let contents; if (this.props.graph) { // Let the componentDidMount or componentDidUpdate method draw the graph // with DagreD3. We just provide the container for the graph here. contents = dom.div({ ref: "container", style: { flex: 1, height: "100%", width: "100%", }, }); } else { contents = dom.div( { id: "shortest-paths-select-node-msg", }, L10N.getStr("shortest-paths.select-node") ); } return dom.div( { id: "shortest-paths", className: "vbox", }, dom.label( { id: "shortest-paths-header", className: "header", }, L10N.getStr("shortest-paths.header") ), contents ); } } module.exports = ShortestPaths;