alm
Version:
The best IDE for TypeScript
614 lines (613 loc) • 26.3 kB
JavaScript
"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;
}