blob: 0c4702237fecc4f204582cadd61b33371e3e1256 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 dagreD3 from "dagre-d3";
import { select, selection, event } from "d3-selection";
import { curveBasis } from "d3-shape";
import { LineageUtils, DataUtils } from "./Utils";
import d3Tip from "d3-tip";
import "./styles/style.scss";
export default class LineageHelper {
constructor(options) {
this.options = {};
this._updateOptions(options);
const { el, manualTrigger = false } = this.options;
if (el === undefined) {
throw new Error("LineageHelper requires el propety to render the graph");
}
this.initReturnObj = {
init: (arg) => this.init(arg),
updateOptions: (options) => this._updateAllOptions(options),
createGraph: (opt = {}) => this._createGraph(this.options, this.graphOptions, opt),
clear: (arg) => this.clear(arg),
refresh: (arg) => this.refresh(arg),
centerAlign: (arg) => this.centerAlign(arg),
exportLineage: (arg) => this.exportLineage(arg),
zoomIn: (arg) => this.zoomIn(arg),
zoomOut: (arg) => this.zoomOut(arg),
zoom: (arg) => this.zoom(arg),
fullScreen: (arg) => this.fullScreen(arg),
searchNode: (arg) => this.searchNode(arg),
displayFullName: (arg) => this.displayFullName(arg),
removeNodeSelection: (arg) => this.removeNodeSelection(arg),
getGraphOptions: () => this.graphOptions,
getNode: (guid, actual) => {
let rObj = null;
if (actual) {
rObj = this.actualData[guid];
} else {
rObj = this.g._nodes[guid];
}
if (rObj) {
rObj = Object.assign({}, rObj);
}
return rObj;
},
getNodes: (guid, actual) => {
let rObj = null;
if (actual) {
rObj = this.actualData;
} else {
rObj = this.g._nodes;
}
if (rObj) {
rObj = Object.assign({}, rObj);
}
return rObj;
},
setNode: this._setGraphNode,
setEdge: this._setGraphEdge
};
if (manualTrigger === false) {
this.init();
}
return this.initReturnObj;
}
/**
* [updateAllOptions]
* @param {[type]}
* @return {[type]}
*/
_updateAllOptions(options) {
Object.assign(this.options, options);
var svgRect = this.svg.node().getBoundingClientRect();
this.graphOptions.width = this.options.width || svgRect.width;
this.graphOptions.height = this.options.height || svgRect.height;
const { svg, width, height, guid } = this.graphOptions;
const { fitToScreen } = this.options;
svg.select("g").node().removeAttribute("transform");
svg.attr("viewBox", "0 0 " + width + " " + height).attr("enable-background", "new 0 0 " + width + " " + height);
this.centerAlign({ fitToScreen, guid });
}
/**
* [updateOptions get the options from user and appedn add it in this,option context]
* @param {[Object]} options [lib options from user]
* @return {[null]} [null]
*/
_updateOptions(options) {
Object.assign(this.options, { filterObj: { isProcessHideCheck: false, isDeletedEntityHideCheck: false } }, options);
}
/**
* [init Start the graph build process]
* @return {[null]} [null]
*/
init() {
const { data = {} } = this.options;
if (data.baseEntityGuid) {
this.guid = data.baseEntityGuid;
}
// Call the initializeGraph method to initlize dagreD3 graphlib
this._initializeGraph();
this._initGraph();
}
/**
* [clear Allows user to clear the graph refrence and dom]
* @return {[type]} [description]
*/
clear() {
if (!this.options.el) {
this.svg.remove();
this.svg = null;
}
this.g = null;
this.graphOptions = {};
}
/**
* [centerAlign Allows user to center the lineage position, without rerender]
* @return {[type]} [description]
*/
centerAlign(opt = {}) {
var svgGroupEl = this.svg.select("g"),
edgePathEl = svgGroupEl.selectAll("g.edgePath");
LineageUtils.centerNode({
...this.graphOptions,
svgGroupEl,
edgePathEl,
...opt
});
}
/**
* [zoomIn description]
* @return {[type]} [description]
*/
zoomIn(opt = {}) {
LineageUtils.zoomIn({ ...this.graphOptions, ...opt });
}
/**
* [zoomOut description]
* @return {[type]} [description]
*/
zoomOut(opt = {}) {
LineageUtils.zoomOut({ ...this.graphOptions, ...opt });
}
/**
* [zoom description]
* @return {[type]} [description]
*/
zoom(opt = {}) {
LineageUtils.zoom({ ...this.graphOptions, ...opt });
}
displayFullName(opt = {}) {
var that = this;
this.g.nodes().forEach(function(v) {
var selectedNodeEl = that.svg.selectAll("g.nodes>g[id='" + v + "']"),
label = that.g.node(v).toolTipLabel;
if (opt.bLabelFullText == true)
selectedNodeEl.select('tspan').text(label);
else
selectedNodeEl.select('tspan').text(label.trunc(18));
});
if (this.selectedNode) {
this.searchNode({ guid: this.selectedNode });
}
}
/**
* [refresh Allows user to rerender the lineage]
* @return {[type]} [description]
*/
refresh(options) {
this.clear();
this._initializeGraph();
this._initGraph({ refresh: true });
this.selectedNode = "";
if (options && options.compactLineageEnabled && options.filterObj) {
var isProcessHideCheck = options.filterObj.isProcessHideCheck,
isDeletedEntityHideCheck = options.filterObj.isDeletedEntityHideCheck;
this._AddFilterNotification(isProcessHideCheck, isDeletedEntityHideCheck);
}
}
/**
* [removeNodeSelection description]
* @return {[type]} [description]
*/
removeNodeSelection() {
this.svg.selectAll("g.node>circle").classed("node-detail-highlight", false);
}
/**
* [searchNode description]
* @return {[type]} [description]
*/
searchNode({ guid, onSearchNode }) {
this.svg.selectAll(".serach-rect").remove();
this.svg.selectAll(".label").attr("stroke", "none");
this.selectedNode = guid;
this.centerAlign({
guid: guid,
onCenterZoomed: function(opts) {
const { selectedNodeEl } = opts;
var oSelectedNode = selectedNodeEl.node().getBBox(),
rectWidth = oSelectedNode.width + 10,
rectXPos = oSelectedNode.x - 5;
selectedNodeEl.select(".label").attr("stroke", "#316132");
selectedNodeEl.select("circle").classed("wobble", true);
selectedNodeEl
.insert("rect", "circle")
.attr("class", "serach-rect")
.attr("stroke", "#37bb9b")
.attr("stroke-width", "2.5px")
.attr("fill", "none")
.attr("x", rectXPos)
.attr("y", -27.5)
.attr("width", rectWidth)
.attr("height", 60);
if (onSearchNode && typeof onSearchNode === "function") {
onSearchNode(opts);
}
},
isSelected: true
});
}
/**
* [exportLineage description]
* @param {Object} options [description]
* @return {[type]} [description]
*/
exportLineage(options = {}) {
let downloadFileName = options.downloadFileName;
if (downloadFileName === undefined) {
let node = this.g._nodes[this.guid];
if (node && node.attributes) {
downloadFileName = `${node.attributes.qualifiedName || node.attributes.name || "lineage_export"}.png`;
} else {
downloadFileName = "export.png";
}
}
LineageUtils.saveSvg({
...this.graphOptions,
downloadFileName: downloadFileName,
onExportLineage: (opt) => {
if (options.onExportLineage) {
onExportLineage(opt);
}
}
});
}
/**
* [fullScreen description]
* @param {Object} options.el } [description]
* @return {[type]} [description]
*/
fullScreen({ el } = {}) {
if (el === undefined) {
throw new Error("LineageHelper requires el propety to apply fullScreen class");
}
const fullScreenEl = select(el);
if (fullScreenEl.classed("fullscreen-mode")) {
fullScreenEl.classed("fullscreen-mode", false);
return false;
} else {
fullScreenEl.classed("fullscreen-mode", true);
return true;
}
}
/**
* [_getValueFromUser description]
* @param {[type]} ref [description]
* @return {[type]} [description]
*/
_getValueFromUser(ref) {
if (ref !== undefined) {
if (typeof ref === "function") {
return ref();
} else {
return ref;
}
}
return;
}
/**
* [initializeGraph initlize the dagreD3 graphlib]
* @return {[null]} [null]
*/
_initializeGraph() {
const { width = "100%", height = "100%", el } = this.options;
// Append the svg using d3.
this.svg = select(el);
if (!(el instanceof SVGElement)) {
this.svg.selectAll("*").remove();
this.svg = this.svg
.append("svg")
.attr("xmlns", "http://www.w3.org/2000/svg")
.attr(" xmlns:xlink", "http://www.w3.org/1999/xlink")
.attr("version", "1.1")
.attr("width", width)
.attr("height", height);
}
// initlize the dagreD3 graphlib
this.g = new dagreD3.graphlib.Graph()
.setGraph(
Object.assign({
nodesep: 50,
ranksep: 90,
rankdir: "LR",
marginx: 20,
marginy: 20,
transition: function transition(selection) {
return selection.transition().duration(500);
}
},
this.options.dagreOptions
)
)
.setDefaultEdgeLabel(function() {
return {};
});
// Create graphOptions for common use
var svgRect = this.svg.node().getBoundingClientRect();
this.actualData = {};
this.graphOptions = {
svg: this.svg,
g: this.g,
dagreD3: dagreD3,
guid: this.guid,
width: this.options.width || svgRect.width,
height: this.options.height || svgRect.height
};
}
/**
* [_initGraph description]
* @return {[type]} [description]
*/
_initGraph({ refresh } = {}) {
if (this.svg) {
this.svg.select("g").remove();
}
let filterObj = this.options.filterObj;
if (this.options.getFilterObj) {
let filterObjVal = this.options.getFilterObj();
if (filterObjVal !== undefined || filterObjVal !== null) {
if (typeof filterObjVal === "object") {
filterObj = filterObjVal;
} else {
throw new Error("getFilterObj expect return type `object`,`null` or `Undefined`");
}
}
}
if (this.options.setDataManually === true) {
return;
} else if (this.options.data === undefined || (this.options.data && this.options.data.relations.length === 0 && _.isEmpty(this.options.data.guidEntityMap))) {
if (this.options.beforeRender) {
this.options.beforeRender();
}
this.svg
.append("text")
.attr("x", "50%")
.attr("y", "50%")
.attr("alignment-baseline", "middle")
.attr("text-anchor", "middle")
.text("No lineage data found");
if (this.options.afterRender) {
this.options.afterRender();
}
return;
}
return DataUtils.generateData({
...this.options,
filterObj: filterObj,
...this.graphOptions,
setGraphNode: this._setGraphNode,
setGraphEdge: this._setGraphEdge
}).then((graphObj) => {
this._createGraph(this.options, this.graphOptions, { refresh });
});
}
/**
* [description]
* @param {[type]} guid [description]
* @param {[type]} nodeData [description]
* @return {[type]} [description]
*/
_setGraphNode = (guid, nodeData) => {
this.actualData[guid] = Object.assign({}, nodeData);
this.g.setNode(guid, nodeData);
};
/**
* [description]
* @param {[type]} fromGuid [description]
* @param {[type]} toGuid [description]
* @param {[type]} opts [description]
* @return {[type]} [description]
*/
_setGraphEdge = (fromGuid, toGuid, opts) => {
this.g.setEdge(fromGuid, toGuid, {
curve: curveBasis,
...opts
});
};
/**
* [_createGraph description]
* @param {Object} options.data [description]
* @param {Boolean} isShowTooltip [description]
* @param {Boolean} isShowHoverPath [description]
* @param {[type]} onLabelClick [description]
* @param {[type]} onPathClick [description]
* @param {[type]} onNodeClick } [description]
* @param {[type]} graphOptions [description]
* @return {[type]} [description]
*/
_createGraph({
data = {},
imgBasePath,
isShowTooltip,
isShowHoverPath,
onLabelClick,
onPathClick,
onNodeClick,
zoom,
fitToScreen,
getToolTipContent,
toolTipTitle
},
graphOptions, { refresh }
) {
if (this.options.beforeRender) {
this.options.beforeRender();
}
this.selectedNode = "";
const that = this,
{ svg, g, width, height } = graphOptions,
isRankdirToBottom = this.options.dagreOptions && this.options.dagreOptions.rankdir === "tb";
if (svg instanceof selection === false) {
throw new Error("svg is not initialized or something went wrong while creatig graph instance");
return;
}
if (g._nodes === undefined || g._nodes.length === 0) {
svg.html('<text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">No relations to display</text>');
return;
}
g.nodes().forEach(function(v) {
var node = g.node(v);
// Round the corners of the nodes
if (node) {
node.rx = node.ry = 5;
}
});
svg.attr("viewBox", "0 0 " + width + " " + height).attr("enable-background", "new 0 0 " + width + " " + height);
var svgGroupEl = svg.append("g");
// Append defs
var defsEl = svg.append("defs");
// Create the renderer
var render = new dagreD3.render();
// Add our custom arrow (a hollow-point)
render.arrows().arrowPoint = function() {
return LineageUtils.arrowPointRender(...arguments, { ...graphOptions });
};
// Render custom img inside shape
render.shapes().img = function() {
return LineageUtils.imgShapeRender(...arguments, {
...graphOptions,
isRankdirToBottom: isRankdirToBottom,
imgBasePath: that._getValueFromUser(imgBasePath),
defsEl
});
};
var tooltip = d3Tip()
.attr("class", "d3-tip")
.offset([10, 0])
.html((d) => {
if (getToolTipContent && typeof getToolTipContent === "function") {
return getToolTipContent(d, g.node(d));
} else {
var value = g.node(d);
var htmlStr = "";
if (toolTipTitle) {
htmlStr = "<h5 style='text-align: center;'>" + toolTipTitle + "</h5>";
} else if (value.id !== this.guid) {
htmlStr = "<h5 style='text-align: center;'>" + (value.isLineage ? "Lineage" : "Impact") + "</h5>";
}
htmlStr += "<h5 class='text-center'><span style='color:#359f89'>" + value.toolTipLabel + "</span></h5> ";
if (value.typeName) {
htmlStr += "<h5 class='text-center'><span>(" + value.typeName + ")</span></h5> ";
}
if (value.queryText) {
htmlStr += "<h5>Query: <span style='color:#359f89'>" + value.queryText + "</span></h5> ";
}
return "<div class='tip-inner-scroll'>" + htmlStr + "</div>";
}
});
svg.call(tooltip);
// if (platform.name !== "IE") {
// this.$(".fontLoader").hide();
// }
render(svgGroupEl, g);
//change text postion
svgGroupEl
.selectAll("g.nodes g.label")
.attr("transform", () => {
if (isRankdirToBottom) {
return "translate(2,-20)";
}
return "translate(2,-38)";
})
.attr("font-size", "10px")
.on("mouseenter", function(d) {
event.preventDefault();
select(this).classed("highlight", true);
})
.on("mouseleave", function(d) {
event.preventDefault();
select(this).classed("highlight", false);
})
.on("click", function(d) {
event.preventDefault();
if (onLabelClick && typeof onLabelClick === "function") {
onLabelClick({ clickedData: d });
}
tooltip.hide(d);
});
svgGroupEl
.selectAll("g.nodes g.node circle")
.on("mouseenter", function(d, index, element) {
that.activeNode = true;
var matrix = this.getScreenCTM().translate(+this.getAttribute("cx"), +this.getAttribute("cy"));
that.svg.selectAll(".node").classed("active", false);
select(this).classed("active", true);
if (that._getValueFromUser(isShowTooltip) && (d.indexOf("more") !== 0)) {
var direction = LineageUtils.getToolTipDirection({ el: this });
tooltip.direction(direction).show(d, this);
}
if (that._getValueFromUser(isShowHoverPath) === false) {
return;
}
LineageUtils.onHoverFade({
opacity: 0.3,
mouseenter: true,
hoveredNode: d,
...graphOptions
});
})
.on("mouseleave", function(d) {
that.activeNode = false;
var nodeEL = this;
setTimeout(function(argument) {
if (!(that.activeTip || that.activeNode)) {
select(nodeEL).classed("active", false);
if (that._getValueFromUser(isShowTooltip)) {
tooltip.hide(d);
}
}
}, 150);
if (that._getValueFromUser(isShowHoverPath) === false) {
return;
}
LineageUtils.onHoverFade({
mouseenter: false,
hoveredNode: d,
...graphOptions
});
})
.on("click", function(d) {
if (event.defaultPrevented) return; // ignore drag
event.preventDefault();
tooltip.hide(d);
svg.selectAll("g.node>circle").classed("node-detail-highlight", false);
select(this).classed("node-detail-highlight", true);
if (onNodeClick && typeof onNodeClick === "function") {
onNodeClick({ clickedData: d, el: this });
}
});
// Bind event on edgePath
var edgePathEl = svgGroupEl.selectAll("g.edgePath");
edgePathEl.selectAll("path.path").on("click", function(d) {
if (onPathClick && typeof onPathClick === "function") {
var pathRelationObj = data.relations.find(function(obj) {
if (obj.fromEntityId === d.v && obj.toEntityId === d.w) {
return true;
}
});
onPathClick({ pathRelationObj, clickedData: d });
}
});
// tooltip hover handle to fix node hover conflict
// select("body").on("mouseover", ".d3-tip", function(el) {
// that.activeTip = true;
// });
// select("body").on("mouseleave", ".d3-tip", function(el) {
// that.activeTip = false;
// svg.selectAll(".node").classed("active", false);
// //tooltip.hide();
// });
// Center the graph
if (zoom !== false) {
LineageUtils.centerNode({
...graphOptions,
fitToScreen,
svgGroupEl,
edgePathEl
});
}
// if (platform.name === "IE") {
// LineageUtils.refreshGraphForIE({
// edgeEl: this.$("svg .edgePath")
// });
// }
LineageUtils.dragNode({
...graphOptions,
edgePathEl
});
if (refresh !== true) {
this._addLegend();
}
if (this.options.afterRender) {
this.options.afterRender();
}
}
_addLegend() {
if (this.options.legends === false) {
return;
}
var container = select(this.options.legendsEl || this.options.el)
.insert("div", ":first-child")
.classed("legends", true);
let span = container.append("span").style("color", "#fb4200");
span.append("i").classed("fa fa-circle-o fa-fw", true);
span.append("span").html("Current Entity");
span = container.append("span").style("color", "#686868");
span.append("i").classed("fa fa-hourglass-half fa-fw", true);
span.append("span").html("In Progress");
span = container.append("span").style("color", "#df9b00");
span.append("i").classed("fa fa-long-arrow-right fa-fw", true);
span.append("span").html("Lineage");
span = container.append("span").style("color", "#fb4200");
span.append("i").classed("fa fa-long-arrow-right fa-fw", true);
span.append("span").html("Impact");
span = container.append("span").classed("notification hide", true).style("color", "#686868");
span.append("i").classed("fa fa-exclamation fa-fw", true);
span.append("span").html("Filtering hides all Expand buttons.");
}
_AddFilterNotification(isProcessHideCheck, isDeletedEntityHideCheck) {
if ((isProcessHideCheck || isDeletedEntityHideCheck)) {
$(this.options.legendsEl).find('.notification').removeClass('hide');
} else {
$(this.options.legendsEl).find('.notification').addClass('hide');
}
}
}