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