| /** |
| * 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. |
| */ |
| |
| define([ |
| "require", |
| "backbone", |
| "hbs!tmpl/graph/RelationshipLayoutView_tmpl", |
| "collection/VLineageList", |
| "models/VEntity", |
| "utils/Utils", |
| "utils/CommonViewFunction", |
| "d3", |
| "d3-tip", |
| "utils/Enums", |
| "utils/UrlLinks", |
| "platform" |
| ], function(require, Backbone, RelationshipLayoutViewtmpl, VLineageList, VEntity, Utils, CommonViewFunction, d3, d3Tip, Enums, UrlLinks, platform) { |
| "use strict"; |
| |
| var RelationshipLayoutView = Backbone.Marionette.LayoutView.extend( |
| /** @lends RelationshipLayoutView */ |
| { |
| _viewName: "RelationshipLayoutView", |
| |
| template: RelationshipLayoutViewtmpl, |
| className: "resizeGraph", |
| /** Layout sub regions */ |
| regions: {}, |
| |
| /** ui selector cache */ |
| ui: { |
| relationshipDetailClose: '[data-id="close"]', |
| searchNode: '[data-id="searchNode"]', |
| relationshipViewToggle: 'input[name="relationshipViewToggle"]', |
| relationshipDetailTable: "[data-id='relationshipDetailTable']", |
| relationshipSVG: "[data-id='relationshipSVG']", |
| relationshipDetailValue: "[data-id='relationshipDetailValue']", |
| zoomControl: "[data-id='zoomControl']", |
| boxClose: '[data-id="box-close"]', |
| noValueToggle: "[data-id='noValueToggle']" |
| }, |
| |
| /** ui events hash */ |
| events: function() { |
| var events = {}; |
| events["click " + this.ui.relationshipDetailClose] = function() { |
| this.toggleInformationSlider({ close: true }); |
| }; |
| events["keyup " + this.ui.searchNode] = "searchNode"; |
| events["click " + this.ui.boxClose] = "toggleBoxPanel"; |
| events["change " + this.ui.relationshipViewToggle] = function(e) { |
| this.relationshipViewToggle(e.currentTarget.checked); |
| }; |
| events["click " + this.ui.noValueToggle] = function(e) { |
| Utils.togglePropertyRelationshipTableEmptyValues({ |
| inputType: this.ui.noValueToggle, |
| tableEl: this.ui.relationshipDetailValue |
| }); |
| }; |
| return events; |
| }, |
| |
| /** |
| * intialize a new RelationshipLayoutView Layout |
| * @constructs |
| */ |
| initialize: function(options) { |
| _.extend(this, _.pick(options, "entity", "entityName", "guid", "actionCallBack", "attributeDefs")); |
| this.graphData = this.createData(this.entity); |
| }, |
| createData: function(entity) { |
| var that = this, |
| links = [], |
| nodes = {}; |
| if (entity && entity.relationshipAttributes) { |
| _.each(entity.relationshipAttributes, function(obj, key) { |
| if (!_.isEmpty(obj)) { |
| links.push({ |
| source: nodes[that.entity.typeName] || |
| (nodes[that.entity.typeName] = _.extend({ name: that.entity.typeName }, { value: entity })), |
| target: nodes[key] || |
| (nodes[key] = _.extend({ |
| name: key |
| }, { value: obj })), |
| value: obj |
| }); |
| } |
| }); |
| } |
| return { nodes: nodes, links: links }; |
| }, |
| onRender: function() { |
| this.ui.zoomControl.hide(); |
| this.$el.addClass("auto-height"); |
| }, |
| onShow: function(argument) { |
| if (this.graphData && _.isEmpty(this.graphData.links)) { |
| this.noRelationship(); |
| } else { |
| this.createGraph(this.graphData); |
| } |
| this.createTable(); |
| }, |
| noRelationship: function() { |
| this.$("svg").html('<text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle">No relationship data found</text>'); |
| }, |
| toggleInformationSlider: function(options) { |
| if (options.open && !this.$(".relationship-details").hasClass("open")) { |
| this.$(".relationship-details").addClass("open"); |
| } else if (options.close && this.$(".relationship-details").hasClass("open")) { |
| d3.selectAll("circle").attr("stroke", "none"); |
| this.$(".relationship-details").removeClass("open"); |
| } |
| }, |
| toggleBoxPanel: function(options) { |
| var el = options && options.el, |
| nodeDetailToggler = options && options.nodeDetailToggler, |
| currentTarget = options.currentTarget; |
| this.$el.find(".show-box-panel").removeClass("show-box-panel"); |
| if (el && el.addClass) { |
| el.addClass("show-box-panel"); |
| } |
| this.$("circle.node-detail-highlight").removeClass("node-detail-highlight"); |
| }, |
| searchNode: function(e) { |
| var $el = $(e.currentTarget); |
| this.updateRelationshipDetails(_.extend({}, $el.data(), { searchString: $el.val() })); |
| }, |
| updateRelationshipDetails: function(options) { |
| var data = options.obj.value, |
| typeName = data.typeName || options.obj.name, |
| searchString = _.escape(options.searchString), |
| listString = "", |
| getEntityTypelist = function(options) { |
| var activeEntityColor = "#4a90e2", |
| deletedEntityColor = "#BB5838", |
| entityTypeHtml = "<pre>", |
| getdefault = function(obj) { |
| var options = obj.options, |
| status = Enums.entityStateReadOnly[options.entityStatus || options.status] ? " deleted-relation" : "", |
| guid = options.guid, |
| entityColor = obj.color, |
| name = obj.name, |
| typeName = options.typeName; |
| if (typeName === "AtlasGlossaryTerm") { |
| return '<li class=' + status + '>' + |
| '<a style="color:' + entityColor + '" href="#!/glossary/' + guid + '?guid=' + guid + '&gType=term&viewType=term&fromView=entity">' + name + ' (' + typeName + ')</a>' + |
| '</li>'; |
| } else { |
| return "<li class=" + status + ">" + |
| "<a style='color:" + entityColor + "' href=#!/detailPage/" + guid + "?tabActive=relationship>" + name + " (" + typeName + ")</a>" + |
| "</li>"; |
| } |
| }, |
| getWithButton = function(obj) { |
| var options = obj.options, |
| status = Enums.entityStateReadOnly[options.entityStatus || options.status] ? " deleted-relation" : "", |
| guid = options.guid, |
| entityColor = obj.color, |
| name = obj.name, |
| typeName = options.typeName, |
| relationship = obj.relationship || false, |
| entity = obj.entity || false, |
| icon = '<i class="fa fa-trash"></i>', |
| title = "Deleted"; |
| if (relationship) { |
| icon = '<i class="fa fa-long-arrow-right"></i>'; |
| status = Enums.entityStateReadOnly[options.relationshipStatus || options.status] ? "deleted-relation" : ""; |
| title = "Relationship Deleted"; |
| } |
| return "<li class=" + status + ">" + |
| "<a style='color:" + entityColor + "' href=#!/detailPage/" + options.guid + "?tabActive=relationship>" + _.escape(name) + " (" + options.typeName + ")</a>" + |
| '<button type="button" title="' + title + '" class="btn btn-sm deleteBtn deletedTableBtn btn-action ">' + icon + '</button>' + |
| "</li>"; |
| }; |
| |
| var name = options.entityName ? options.entityName : Utils.getName(options, "displayText"); |
| if (options.entityStatus == "ACTIVE") { |
| if (options.relationshipStatus == "ACTIVE") { |
| entityTypeHtml = getdefault({ |
| color: activeEntityColor, |
| options: options, |
| name: name |
| }); |
| } else if (options.relationshipStatus == "DELETED") { |
| entityTypeHtml = getWithButton({ |
| color: activeEntityColor, |
| options: options, |
| name: name, |
| relationship: true |
| }); |
| } |
| } else if (options.entityStatus == "DELETED") { |
| entityTypeHtml = getWithButton({ |
| color: deletedEntityColor, |
| options: options, |
| name: name, |
| entity: true |
| }); |
| } else { |
| entityTypeHtml = getdefault({ |
| color: activeEntityColor, |
| options: options, |
| name: name |
| }); |
| } |
| return entityTypeHtml + "</pre>"; |
| }; |
| this.ui.searchNode.hide(); |
| this.$("[data-id='typeName']").text(typeName); |
| var getElement = function(options) { |
| var name = options.entityName ? options.entityName : Utils.getName(options, "displayText"); |
| var entityTypeButton = getEntityTypelist(options); |
| return entityTypeButton; |
| }; |
| if (_.isArray(data)) { |
| if (data.length > 1) { |
| this.ui.searchNode.show(); |
| } |
| _.each(_.sortBy(data, "displayText"), function(val) { |
| var name = Utils.getName(val, "displayText"), |
| valObj = _.extend({}, val, { entityName: name }); |
| if (searchString) { |
| if (name.search(new RegExp(searchString, "i")) != -1) { |
| listString += getElement(valObj); |
| } else { |
| return; |
| } |
| } else { |
| listString += getElement(valObj); |
| } |
| }); |
| } else { |
| listString += getElement(data); |
| } |
| this.$("[data-id='entityList']").html(listString); |
| }, |
| createGraph: function(data) { |
| //Ref - http://bl.ocks.org/fancellu/2c782394602a93921faff74e594d1bb1 |
| |
| var that = this, |
| width = this.$("svg").width(), |
| height = this.$("svg").height(), |
| nodes = d3.values(data.nodes), |
| links = data.links; |
| |
| var activeEntityColor = "#00b98b", |
| deletedEntityColor = "#BB5838", |
| defaultEntityColor = "#e0e0e0", |
| selectedNodeColor = "#4a90e2"; |
| |
| var svg = d3 |
| .select(this.$("svg")[0]) |
| .attr("viewBox", "0 0 " + width + " " + height) |
| .attr("enable-background", "new 0 0 " + width + " " + height), |
| node, |
| path; |
| |
| var container = svg |
| .append("g") |
| .attr("id", "container") |
| .attr("transform", "translate(0,0)scale(1,1)"); |
| |
| var zoom = d3 |
| .zoom() |
| .scaleExtent([0.1, 4]) |
| .on("zoom", function() { |
| container.attr("transform", d3.event.transform); |
| }); |
| |
| svg.call(zoom).on("dblclick.zoom", null); |
| |
| container |
| .append("svg:defs") |
| .selectAll("marker") |
| .data(["deletedLink", "activeLink"]) // Different link/path types can be defined here |
| .enter() |
| .append("svg:marker") // This section adds in the arrows |
| .attr("id", String) |
| .attr("viewBox", "-0 -5 10 10") |
| .attr("refX", 10) |
| .attr("refY", -0.5) |
| .attr("orient", "auto") |
| .attr("markerWidth", 6) |
| .attr("markerHeight", 6) |
| .append("svg:path") |
| .attr("d", "M 0,-5 L 10 ,0 L 0,5") |
| .attr("fill", function(d) { |
| return d == "deletedLink" ? deletedEntityColor : activeEntityColor; |
| }) |
| .style("stroke", "none"); |
| |
| var forceLink = d3 |
| .forceLink() |
| .id(function(d) { |
| return d.id; |
| }) |
| .distance(function(d) { |
| return 100; |
| }) |
| .strength(1); |
| |
| var simulation = d3 |
| .forceSimulation() |
| .force("link", forceLink) |
| .force("charge", d3.forceManyBody()) |
| .force("center", d3.forceCenter(width / 2, height / 2)); |
| |
| update(); |
| |
| function update() { |
| path = container |
| .append("svg:g") |
| .selectAll("path") |
| .data(links) |
| .enter() |
| .append("svg:path") |
| .attr("class", "relatioship-link") |
| .attr("stroke", function(d) { |
| return getPathColor({ data: d, type: "path" }); |
| }) |
| .attr("marker-end", function(d) { |
| return "url(#" + (isAllEntityRelationDeleted({ data: d }) ? "deletedLink" : "activeLink") + ")"; |
| }); |
| |
| node = container |
| .selectAll(".node") |
| .data(nodes) |
| .enter() |
| .append("g") |
| .attr("class", "node") |
| .on("mousedown", function() { |
| console.log(d3.event); |
| d3.event.preventDefault(); |
| }) |
| .on("click", function(d) { |
| if (d3.event.defaultPrevented) return; // ignore drag |
| if (d && d.value && d.value.guid == that.guid) { |
| that.ui.boxClose.trigger("click"); |
| return; |
| } |
| that.toggleBoxPanel({ el: that.$(".relationship-node-details") }); |
| that.ui.searchNode.data({ obj: d }); |
| $(this) |
| .find("circle") |
| .addClass("node-detail-highlight"); |
| that.updateRelationshipDetails({ obj: d }); |
| }) |
| .call( |
| d3 |
| .drag() |
| .on("start", dragstarted) |
| .on("drag", dragged) |
| ); |
| |
| var circleContainer = node.append("g"); |
| |
| circleContainer |
| .append("circle") |
| .attr("cx", 0) |
| .attr("cy", 0) |
| .attr("r", function(d) { |
| d.radius = 25; |
| return d.radius; |
| }) |
| .attr("fill", function(d) { |
| if (d && d.value && d.value.guid == that.guid) { |
| if (isAllEntityRelationDeleted({ data: d, type: "node" })) { |
| return deletedEntityColor; |
| } else { |
| return selectedNodeColor; |
| } |
| } else if (isAllEntityRelationDeleted({ data: d, type: "node" })) { |
| return deletedEntityColor; |
| } else { |
| return activeEntityColor; |
| } |
| }) |
| .attr("typename", function(d) { |
| return d.name; |
| }); |
| |
| circleContainer |
| .append("text") |
| .attr("x", 0) |
| .attr("y", 0) |
| .attr("dy", 25 - 17) |
| .attr("text-anchor", "middle") |
| .style("font-family", "FontAwesome") |
| .style("font-size", function(d) { |
| return "25px"; |
| }) |
| .text(function(d) { |
| var iconObj = Enums.graphIcon[d.name]; |
| if (iconObj && iconObj.textContent) { |
| return iconObj.textContent; |
| } else { |
| if (d && _.isArray(d.value) && d.value.length > 1) { |
| return "\uf0c5"; |
| } else { |
| return "\uf016"; |
| } |
| } |
| }) |
| .attr("fill", function(d) { |
| return "#fff"; |
| }); |
| |
| var countBox = circleContainer.append("g"); |
| |
| countBox |
| .append("circle") |
| .attr("cx", 18) |
| .attr("cy", -20) |
| .attr("r", function(d) { |
| if (_.isArray(d.value) && d.value.length > 1) { |
| return 10; |
| } |
| }); |
| |
| countBox |
| .append("text") |
| .attr("dx", 18) |
| .attr("dy", -16) |
| .attr("text-anchor", "middle") |
| .attr("fill", defaultEntityColor) |
| .text(function(d) { |
| if (_.isArray(d.value) && d.value.length > 1) { |
| return d.value.length; |
| } |
| }); |
| |
| node.append("text") |
| .attr("x", -15) |
| .attr("y", "35") |
| .text(function(d) { |
| return d.name; |
| }); |
| |
| simulation.nodes(nodes).on("tick", ticked); |
| |
| simulation.force("link").links(links); |
| } |
| |
| function ticked() { |
| path.attr("d", function(d) { |
| var diffX = d.target.x - d.source.x, |
| diffY = d.target.y - d.source.y, |
| // Length of path from center of source node to center of target node |
| pathLength = Math.sqrt(diffX * diffX + diffY * diffY), |
| // x and y distances from center to outside edge of target node |
| offsetX = (diffX * d.target.radius) / pathLength, |
| offsetY = (diffY * d.target.radius) / pathLength; |
| |
| return "M" + d.source.x + "," + d.source.y + "A" + pathLength + "," + pathLength + " 0 0,1 " + (d.target.x - offsetX) + "," + (d.target.y - offsetY); |
| }); |
| |
| node.attr("transform", function(d) { |
| return "translate(" + d.x + "," + d.y + ")"; |
| }); |
| } |
| |
| function dragstarted(d) { |
| d3.event.sourceEvent.stopPropagation(); |
| if (d && d.value && d.value.guid != that.guid) { |
| if (!d3.event.active) simulation.alphaTarget(0.3).restart(); |
| d.fx = d.x; |
| d.fy = d.y; |
| } |
| } |
| |
| function dragged(d) { |
| if (d && d.value && d.value.guid != that.guid) { |
| d.fx = d3.event.x; |
| d.fy = d3.event.y; |
| } |
| } |
| |
| function getPathColor(options) { |
| return isAllEntityRelationDeleted(options) ? deletedEntityColor : activeEntityColor; |
| } |
| |
| function isAllEntityRelationDeleted(options) { |
| var data = options.data, |
| type = options.type; |
| var d = $.extend(true, {}, data); |
| if (d && !_.isArray(d.value)) { |
| d.value = [d.value]; |
| } |
| |
| return ( |
| _.findIndex(d.value, function(val) { |
| if (type == "node") { |
| return (val.entityStatus || val.status) == "ACTIVE"; |
| } else { |
| return val.relationshipStatus == "ACTIVE"; |
| } |
| }) == -1 |
| ); |
| } |
| var zoomClick = function() { |
| var scaleFactor = 0.8; |
| if (this.id === 'zoom_in') { |
| scaleFactor = 1.3; |
| } |
| zoom.scaleBy(svg.transition().duration(750), scaleFactor); |
| } |
| |
| d3.selectAll(this.$('.lineageZoomButton')).on('click', zoomClick); |
| }, |
| createTable: function() { |
| this.entityModel = new VEntity({}); |
| var table = CommonViewFunction.propertyTable({ |
| scope: this, |
| valueObject: this.entity.relationshipAttributes, |
| attributeDefs: this.attributeDefs |
| }); |
| this.ui.relationshipDetailValue.html(table); |
| Utils.togglePropertyRelationshipTableEmptyValues({ |
| inputType: this.ui.noValueToggle, |
| tableEl: this.ui.relationshipDetailValue |
| }); |
| }, |
| relationshipViewToggle: function(checked) { |
| this.ui.relationshipDetailTable.toggleClass("visible invisible"); |
| this.ui.relationshipSVG.toggleClass("visible invisible"); |
| |
| if (checked) { |
| this.ui.zoomControl.hide(); |
| this.$el.addClass("auto-height"); |
| } else { |
| this.ui.zoomControl.show(); |
| this.$el.removeClass("auto-height"); |
| } |
| } |
| } |
| ); |
| return RelationshipLayoutView; |
| }); |