| /** |
| * 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'); |
| } |
| } |
| } |