UNPKG

alm

Version:

The best IDE for TypeScript

614 lines (613 loc) 26.3 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); var ui = require("../../ui"); var csx = require("../../base/csx"); var React = require("react"); var socketClient_1 = require("../../../socket/socketClient"); var utils = require("../../../common/utils"); var d3 = require("d3"); var $ = require("jquery"); var styles = require("../../styles/styles"); var clipboard_1 = require("../../components/clipboard"); var typestyle = require("typestyle"); var EOL = '\n'; /** * The styles */ require('./dependencyView.less'); var controlRootStyle = { pointerEvents: 'none', }; var controlRightStyle = { width: '200px', padding: '10px', overflow: 'auto', wordBreak: 'break-all', pointerEvents: 'all', }; var controlItemClassName = typestyle.style({ pointerEvents: 'auto', padding: '.4rem', transition: 'background .2s', background: 'rgba(200,200,200,.05)', $nest: { '&:hover': { background: 'rgba(200,200,200,.25)', } } }); var cycleHeadingStyle = { fontSize: '1.2rem', }; var DependencyView = /** @class */ (function (_super) { __extends(DependencyView, _super); function DependencyView(props) { var _this = _super.call(this, props) || this; _this.handleKey = function (e) { var unicode = e.charCode; if (String.fromCharCode(unicode).toLowerCase() === "r") { _this.loadData(); } }; _this.loadData = function () { _this.refs.graphRoot.innerHTML = ''; return socketClient_1.server.getDependencies({}).then(function (res) { // Create the graph renderer _this.graphRenderer = new GraphRenderer({ dependencies: res.links, measureSizeRoot: $(_this.refs.root), graphRoot: $(_this.refs.graphRoot), display: function (node) { } }); // get the cycles var cycles = _this.graphRenderer.d3Graph.cycles(); _this.setState({ cycles: cycles }); }); }; _this.zoomIn = function (e) { e.preventDefault(); if (!_this.graphRenderer) return; _this.graphRenderer.zoomIn(); }; _this.zoomOut = function (e) { e.preventDefault(); if (!_this.graphRenderer) return; _this.graphRenderer.zoomOut(); }; _this.zoomFit = function (e) { e.preventDefault(); if (!_this.graphRenderer) return; _this.graphRenderer.zoomFit(); }; /** * TAB implementation */ _this.resize = function () { _this.graphRenderer && _this.graphRenderer.resize(); }; _this.focus = function () { _this.refs.root.focus(); // if its not there its because an XHR is lagging and it will show up when that xhr completes anyways _this.graphRenderer && _this.graphRenderer.resize(); }; _this.save = function () { }; _this.close = function () { }; _this.gotoPosition = function (position) { }; _this.search = { doSearch: function (options) { _this.graphRenderer && _this.graphRenderer.applyFilter(options.query); }, hideSearch: function () { _this.graphRenderer && _this.graphRenderer.clearFilter(); }, findNext: function (options) { }, findPrevious: function (options) { }, replaceNext: function (_a) { var newText = _a.newText; }, replacePrevious: function (_a) { var newText = _a.newText; }, replaceAll: function (_a) { var newText = _a.newText; } }; _this.filePath = utils.getFilePathFromUrl(props.url); _this.state = { cycles: [] }; return _this; } DependencyView.prototype.componentDidMount = function () { var _this = this; this.loadData(); this.disposible.add(socketClient_1.cast.activeProjectConfigDetailsUpdated.on(function () { _this.loadData(); })); var focused = function () { _this.props.onFocused(); }; this.refs.root.addEventListener('focus', focused); this.disposible.add({ dispose: function () { _this.refs.root.removeEventListener('focus', focused); } }); // Listen to tab events var api = this.props.api; this.disposible.add(api.resize.on(this.resize)); this.disposible.add(api.focus.on(this.focus)); this.disposible.add(api.save.on(this.save)); this.disposible.add(api.close.on(this.close)); this.disposible.add(api.gotoPosition.on(this.gotoPosition)); // Listen to search tab events this.disposible.add(api.search.doSearch.on(this.search.doSearch)); this.disposible.add(api.search.hideSearch.on(this.search.hideSearch)); this.disposible.add(api.search.findNext.on(this.search.findNext)); this.disposible.add(api.search.findPrevious.on(this.search.findPrevious)); this.disposible.add(api.search.replaceNext.on(this.search.replaceNext)); this.disposible.add(api.search.replacePrevious.on(this.search.replacePrevious)); this.disposible.add(api.search.replaceAll.on(this.search.replaceAll)); }; DependencyView.prototype.render = function () { var hasCycles = !!this.state.cycles.length; var cyclesMessages = hasCycles ? this.state.cycles.map(function (cycle, i) { var cycleText = cycle.join(' ⬅️ '); return (React.createElement("div", { key: i, className: controlItemClassName }, React.createElement("div", { style: cycleHeadingStyle }, " ", i + 1, ") Cycle ", React.createElement(clipboard_1.Clipboard, { text: cycleText })), React.createElement("div", null, cycleText))); }) : React.createElement("div", { key: -1, className: controlItemClassName }, "No cycles \uD83C\uDF39"); return (React.createElement("div", { ref: "root", tabIndex: 0, className: "dependency-view", style: csx.extend(csx.vertical, csx.flex, csx.newLayerParent, styles.someChildWillScroll), onKeyPress: this.handleKey }, React.createElement("div", { ref: "graphRoot", style: csx.extend(csx.vertical, csx.flex) }), React.createElement("div", { ref: "controlRoot", className: "graph-controls", style: csx.extend(csx.newLayer, csx.horizontal, csx.endJustified, controlRootStyle) }, React.createElement("div", { style: csx.extend(csx.vertical, controlRightStyle) }, React.createElement("div", { className: "control-zoom " + controlItemClassName }, React.createElement("a", { className: "control-zoom-in", href: "#", title: "Zoom in", onClick: this.zoomIn }), React.createElement("a", { className: "control-zoom-out", href: "#", title: "Zoom out", onClick: this.zoomOut }), React.createElement("a", { className: "control-fit", href: "#", title: "Fit", onClick: this.zoomFit })), cyclesMessages)), React.createElement("div", { "data-comment": "Tip", style: csx.extend(styles.Tip.root, csx.content) }, "Tap ", React.createElement("span", { style: styles.Tip.keyboardShortCutStyle }, "R"), " to refresh"))); }; return DependencyView; }(ui.BaseComponent)); exports.DependencyView = DependencyView; var prefixes = { circle: 'circle' }; var GraphRenderer = /** @class */ (function () { function GraphRenderer(config) { var _this = this; this.config = config; this.graphWidth = 0; this.graphHeight = 0; // Use elliptical arc path segments to doubly-encode directionality. this.tick = function () { function transform(d) { return "translate(" + d.x + "," + d.y + ")"; } _this.links.attr("d", linkArc); _this.nodes.attr("transform", transform); _this.text.attr("transform", transform); }; this.applyFilter = utils.debounce(function (val) { if (!val) { _this.clearFilter(); return; } else { _this.nodes.classed('filtered-out', true); _this.links.classed('filtered-out', true); _this.text.classed('filtered-out', true); var filteredNodes = _this.graph.selectAll("circle[data-name*=\"" + _this.htmlName({ name: val }) + "\"]"); filteredNodes.classed('filtered-out', false); var filteredLinks = _this.graph.selectAll("[data-source*=\"" + _this.htmlName({ name: val }) + "\"][data-target*=\"" + _this.htmlName({ name: val }) + "\"]"); filteredLinks.classed('filtered-out', false); var filteredText = _this.graph.selectAll("text[data-name*=\"" + _this.htmlName({ name: val }) + "\"]"); filteredText.classed('filtered-out', false); } }, 250); this.clearFilter = function () { _this.nodes.classed('filtered-out', false); _this.links.classed('filtered-out', false); _this.text.classed('filtered-out', false); }; /** * Layout */ this.resize = function () { _this.graphWidth = _this.config.measureSizeRoot.width(); _this.graphHeight = _this.config.measureSizeRoot.height(); _this.svgRoot.attr("width", _this.graphWidth) .attr("height", _this.graphHeight); _this.layout.size([_this.graphWidth, _this.graphHeight]) .resume(); }; this.centerGraph = function () { var centerTranslate = [ (_this.graphWidth / 4), (_this.graphHeight / 4), ]; _this.zoom.translate(centerTranslate); // Render transition _this.transitionScale(); }; this.zoomIn = function () { _this.zoomCenter(1); }; this.zoomOut = function () { _this.zoomCenter(-1); }; this.zoomFit = function () { _this.zoom.scale(0.4); _this.centerGraph(); }; var d3Root = d3.select(config.graphRoot[0]); var self = this; // Compute the distinct nodes from the links. var d3NodeLookup = {}; var d3links = config.dependencies.map(function (link) { var source = d3NodeLookup[link.sourcePath] || (d3NodeLookup[link.sourcePath] = { name: link.sourcePath }); var target = d3NodeLookup[link.targetPath] || (d3NodeLookup[link.targetPath] = { name: link.targetPath }); return { source: source, target: target }; }); // Calculate all the good stuff this.d3Graph = new D3Graph(d3links); // setup weights based on degrees Object.keys(d3NodeLookup).forEach(function (name) { var node = d3NodeLookup[name]; node.weight = self.d3Graph.avgDeg(node); }); // Setup zoom this.zoom = d3.behavior.zoom() .scale(0.4) .scaleExtent([.1, 6]) .on("zoom", onZoomChanged); this.svgRoot = d3Root.append("svg") .call(this.zoom); this.graph = this.svgRoot .append('svg:g'); this.layout = d3.layout.force() .nodes(d3.values(d3NodeLookup)) .links(d3links) .gravity(.05) .linkDistance(function (link) { return (self.d3Graph.difference(link)) * 200; }) .charge(-900) .on("tick", this.tick) .start(); var drag = this.layout.drag() .on("dragstart", dragstart); /** resize initially and setup for resize */ this.resize(); this.centerGraph(); function onZoomChanged() { self.graph.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); } // Per-type markers, as they don't inherit styles. self.graph.append("defs").selectAll("marker") .data(["regular"]) .enter().append("marker") .attr("id", function (d) { return d; }) .attr("viewBox", "0 -5 10 10") .attr("refX", 15) .attr("refY", -1.5) .attr("markerWidth", 6) .attr("markerHeight", 6) .attr("orient", "auto") .append("path") .attr("d", "M0,-5L10,0L0,5"); this.links = self.graph.append("g").selectAll("path") .data(this.layout.links()) .enter().append("path") .attr("class", function (d) { return "link"; }) .attr("data-target", function (o) { return self.htmlName(o.target); }) .attr("data-source", function (o) { return self.htmlName(o.source); }) .attr("marker-end", function (d) { return "url(#regular)"; }); this.nodes = self.graph.append("g").selectAll("circle") .data(this.layout.nodes()) .enter().append("circle") .attr("class", function (d) { return formatClassName(prefixes.circle, d); }) // Store class name for easier later lookup .attr("data-name", function (o) { return self.htmlName(o); }) // Store for easier later lookup .attr("r", function (d) { return Math.max(d.weight, 3); }) .classed("inonly", function (d) { return self.d3Graph.inOnly(d); }) .classed("outonly", function (d) { return self.d3Graph.outOnly(d); }) .classed("circular", function (d) { return self.d3Graph.isCircular(d); }) .call(drag) .on("dblclick", dblclick) // Unstick .on("mouseover", function (d) { onNodeMouseOver(d); }) .on("mouseout", function (d) { onNodeMouseOut(d); }); this.text = self.graph.append("g").selectAll("text") .data(this.layout.nodes()) .enter().append("text") .attr("x", 8) .attr("y", ".31em") .attr("data-name", function (o) { return self.htmlName(o); }) .text(function (d) { return d.name; }); function onNodeMouseOver(d) { // Highlight circle var elm = findElementByNode(prefixes.circle, d); elm.classed("hovering", true); updateNodeTransparencies(d, true); } function onNodeMouseOut(d) { // Highlight circle var elm = findElementByNode(prefixes.circle, d); elm.classed("hovering", false); updateNodeTransparencies(d, false); } var findElementByNode = function (prefix, node) { var selector = '.' + formatClassName(prefix, node); return self.graph.select(selector); }; function updateNodeTransparencies(d, fade) { if (fade === void 0) { fade = true; } // clean self.nodes.classed('not-hovering', false); self.nodes.classed('dimmed', false); if (fade) { self.nodes.each(function (o) { if (!self.d3Graph.isConnected(d, o)) { this.classList.add('not-hovering'); this.classList.add('dimmed'); } }); } // Clean self.graph.selectAll('path.link').attr('data-show', '') .classed('outgoing', false) .attr('marker-end', fade ? '' : 'url(#regular)') .classed('incomming', false) .classed('dimmed', fade); self.links.each(function (o) { if (o.source.name === d.name) { this.classList.remove('dimmed'); // Highlight target of the link var elmNodes = self.graph.selectAll('.' + formatClassName(prefixes.circle, o.target)); elmNodes.attr('fill-opacity', 1); elmNodes.attr('stroke-opacity', 1); elmNodes.classed('dimmed', false); // Highlight arrows var outgoingLink = self.graph.selectAll('path.link[data-source="' + self.htmlName(o.source) + '"]'); outgoingLink.attr('data-show', 'true'); outgoingLink.attr('marker-end', 'url(#regular)'); outgoingLink.classed('outgoing', true); } else if (o.target.name === d.name) { this.classList.remove('dimmed'); // Highlight arrows var incommingLink = self.graph.selectAll('path.link[data-target="' + self.htmlName(o.target) + '"]'); incommingLink.attr('data-show', 'true'); incommingLink.attr('marker-end', 'url(#regular)'); incommingLink.classed('incomming', true); } }); self.text.classed("dimmed", function (o) { if (!fade) return false; if (self.d3Graph.isConnected(d, o)) return false; return true; }); } // Helpers function formatClassName(prefix, object) { return prefix + '-' + self.htmlName(object); } function dragstart(d) { d.fixed = true; // http://bl.ocks.org/mbostock/3750558 d3.event.sourceEvent.stopPropagation(); // http://bl.ocks.org/mbostock/6123708 d3.select(this).classed("fixed", true); } function dblclick(d) { d3.select(this).classed("fixed", d.fixed = false); } } /** Modifed from http://bl.ocks.org/linssen/7352810 */ GraphRenderer.prototype.zoomCenter = function (direction) { var factor = 0.3, target_zoom = 1, center = [this.graphWidth / 2, this.graphHeight / 2], extent = this.zoom.scaleExtent(), translate = this.zoom.translate(), translate0 = [], l = [], view = { x: translate[0], y: translate[1], k: this.zoom.scale() }; target_zoom = this.zoom.scale() * (1 + factor * direction); if (target_zoom < extent[0] || target_zoom > extent[1]) { return false; } translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k]; view.k = target_zoom; l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y]; view.x += center[0] - l[0]; view.y += center[1] - l[1]; this.zoom.scale(view.k); this.zoom.translate([view.x, view.y]); this.transitionScale(); }; /** * Helpers */ GraphRenderer.prototype.htmlName = function (object) { return object.name.replace(/(\.|\/)/gi, '-'); }; GraphRenderer.prototype.transitionScale = function () { this.graph.transition() .duration(500) .attr("transform", "translate(" + this.zoom.translate() + ")" + " scale(" + this.zoom.scale() + ")"); }; return GraphRenderer; }()); /** * A class to do analysis on D3 links array * Degree : The number of connections * Bit of a lie about degrees : 0 is changed to 1 intentionally */ var D3Graph = /** @class */ (function () { function D3Graph(links) { var _this = this; this.links = links; this.inDegLookup = {}; this.outDegLookup = {}; this.linkedByName = {}; this.targetsBySourceName = {}; this.circularPaths = []; links.forEach(function (l) { if (!_this.inDegLookup[l.target.name]) _this.inDegLookup[l.target.name] = 2; else _this.inDegLookup[l.target.name]++; if (!_this.outDegLookup[l.source.name]) _this.outDegLookup[l.source.name] = 2; else _this.outDegLookup[l.source.name]++; // Build linked lookup for quick connection checks _this.linkedByName[l.source.name + "," + l.target.name] = 1; // Build an adjacency list if (!_this.targetsBySourceName[l.source.name]) _this.targetsBySourceName[l.source.name] = []; _this.targetsBySourceName[l.source.name].push(l.target); }); // Taken from madge this.findCircular(); } D3Graph.prototype.inDeg = function (node) { return this.inDegLookup[node.name] ? this.inDegLookup[node.name] : 1; }; D3Graph.prototype.outDeg = function (node) { return this.outDegLookup[node.name] ? this.outDegLookup[node.name] : 1; }; D3Graph.prototype.avgDeg = function (node) { return (this.inDeg(node) + this.outDeg(node)) / 2; }; D3Graph.prototype.isConnected = function (a, b) { return this.linkedByName[a.name + "," + b.name] || this.linkedByName[b.name + "," + a.name] || a.name == b.name; }; /** how different are the two nodes in the link */ D3Graph.prototype.difference = function (link) { // take file path into account: return utils.relative(link.source.name, link.target.name).split('/').length; }; D3Graph.prototype.inOnly = function (node) { return !this.outDegLookup[node.name] && this.inDegLookup[node.name]; }; D3Graph.prototype.outOnly = function (node) { return !this.inDegLookup[node.name] && this.outDegLookup[node.name]; }; /** * Get path to the circular dependency. */ D3Graph.prototype.getPath = function (parent, unresolved) { var parentVisited = false; return Object.keys(unresolved).filter(function (module) { if (module === parent.name) { parentVisited = true; } return parentVisited && unresolved[module]; }); }; /** * A circular dependency is occurring when we see a software package * more than once, unless that software package has all its dependencies resolved. */ D3Graph.prototype.resolver = function (sourceName, resolved, unresolved) { var _this = this; unresolved[sourceName] = true; if (this.targetsBySourceName[sourceName]) { this.targetsBySourceName[sourceName].forEach(function (dependency) { if (!resolved[dependency.name]) { if (unresolved[dependency.name]) { _this.circularPaths.push(_this.getPath(dependency, unresolved)); return; } _this.resolver(dependency.name, resolved, unresolved); } }); } resolved[sourceName] = true; unresolved[sourceName] = false; }; /** * Finds all circular dependencies for the given modules. */ D3Graph.prototype.findCircular = function () { var _this = this; var resolved = {}, unresolved = {}; Object.keys(this.targetsBySourceName).forEach(function (sourceName) { _this.resolver(sourceName, resolved, unresolved); }); }; ; /** Check if the given module is part of a circular dependency */ D3Graph.prototype.isCircular = function (node) { var cyclic = false; this.circularPaths.some(function (path) { if (path.indexOf(node.name) >= 0) { cyclic = true; return true; } return false; }); return cyclic; }; D3Graph.prototype.cycles = function () { return this.circularPaths; }; return D3Graph; }()); /** modified version of http://stackoverflow.com/a/26616564/390330 Takes weight into account */ function linkArc(d) { var targetX = d.target.x; var targetY = d.target.y; var sourceX = d.source.x; var sourceY = d.source.y; var theta = Math.atan((targetX - sourceX) / (targetY - sourceY)); var phi = Math.atan((targetY - sourceY) / (targetX - sourceX)); var sinTheta = d.source.weight / 2 * Math.sin(theta); var cosTheta = d.source.weight / 2 * Math.cos(theta); var sinPhi = (d.target.weight - 6) * Math.sin(phi); var cosPhi = (d.target.weight - 6) * Math.cos(phi); // Set the position of the link's end point at the source node // such that it is on the edge closest to the target node if (d.target.y > d.source.y) { sourceX = sourceX + sinTheta; sourceY = sourceY + cosTheta; } else { sourceX = sourceX - sinTheta; sourceY = sourceY - cosTheta; } // Set the position of the link's end point at the target node // such that it is on the edge closest to the source node if (d.source.x > d.target.x) { targetX = targetX + cosPhi; targetY = targetY + sinPhi; } else { targetX = targetX - cosPhi; targetY = targetY - sinPhi; } // Draw an arc between the two calculated points var dx = targetX - sourceX, dy = targetY - sourceY, dr = Math.sqrt(dx * dx + dy * dy); return "M" + sourceX + "," + sourceY + "A" + dr + "," + dr + " 0 0,1 " + targetX + "," + targetY; }