/** * 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-tip', 'utils/Enums', 'utils/UrlLinks', 'platform' ], function(require, Backbone, RelationshipLayoutViewtmpl, VLineageList, VEntity, Utils, CommonViewFunction, 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"]' }, /** 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) }; 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() {}, 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 = options.searchString, listString = ""; this.ui.searchNode.hide(); this.$("[data-id='typeName']").text(typeName); var getElement = function(options) { var name = options.entityName ? options.entityName : Utils.getName(options, "displayText"); return "<li class=" + (Enums.entityStateReadOnly[options.entityStatus || options.status] ? "deleted-relation" : '') + "><a href=#!/detailPage/" + options.guid + "?tabActive=relationship>" + _.escape(name) + " (" + options.typeName + ")</a>" + '<button type="button" title="Deleted" class="btn btn-sm deleteBtn deletedTableBtn ' + (Enums.entityStateReadOnly[options.entityStatus || options.status] ? "" : 'hide') + '"><i class="fa fa-trash"></i></button>' + "</li>"; } 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) { var that = this, width = this.$('svg').width(), height = this.$('svg').height(); var scale = 1.0, activeEntityColor = "#00b98b", deletedEntityColor = "#BB5838", defaultEntityColor = "#e0e0e0"; var force = d3.layout.force() .nodes(d3.values(data.nodes)) .links(data.links) .size([width, height]) .linkDistance(200) .gravity(0.0) .friction(0.1) .charge(function(d) { var charge = -500; if (d.index === 0) charge = 100 return charge; }) .on("tick", tick) .start(); var zoom = d3.behavior.zoom() .scale(scale) .scaleExtent([1, 5]) .on("zoom", zoomed); function zoomed() { container.attr("transform", "translate(" + zoom.translate() + ")" + "scale(" + zoom.scale() + ")" ); } function interpolateZoom(translate, scale) { var self = this; return d3.transition().duration(350).tween("zoom", function() { var iTranslate = d3.interpolate(zoom.translate(), translate), iScale = d3.interpolate(zoom.scale(), scale); return function(t) { zoom .scale(iScale(t)) .translate(iTranslate(t)); zoomed(); }; }); } function zoomClick() { var clicked = d3.event.target, direction = 1, factor = 0.5, target_zoom = 1, center = [width / 2, height / 2], extent = zoom.scaleExtent(), translate = zoom.translate(), translate0 = [], l = [], view = { x: translate[0], y: translate[1], k: zoom.scale() }; d3.event.preventDefault(); direction = (this.id === 'zoom_in') ? 1 : -1; target_zoom = 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]; interpolateZoom([view.x, view.y], view.k); } d3.selectAll(this.$('.lineageZoomButton')).on('click', zoomClick); var svg = d3.select(this.$("svg")[0]) .attr("viewBox", "0 0 " + width + " " + height) .attr("enable-background", "new 0 0 " + width + " " + height) .call(zoom) .on("dblclick.zoom", null), drag = force.drag() .on("dragstart", dragstart); var container = svg.append("g") .attr("id", "container") .attr("transform", "translate(0,0)scale(1,1)"); // build the arrow. 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("markerWidth", 6) .attr("markerHeight", 6) .attr("orient", "auto") .append("svg:path") .attr("d", "M0,-5L10,0L0,5") .attr("fill", function(d) { return d == "deletedLink" ? deletedEntityColor : activeEntityColor; }); 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); } // add the links and the arrows var path = container.append("svg:g").selectAll("path") .data(force.links()) .enter().append("svg:path") // .attr("class", function(d) { return "link " + d.type; }) .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") + ")"; }); // define the nodes var node = container.selectAll(".node") .data(force.nodes()) .enter().append("g") .attr("class", "node") .on('touchstart', function(d) { if (d && d.value && d.value.guid != that.guid) { d3.event.stopPropagation(); } }) .on('mousedown', function(d) { if (d && d.value && d.value.guid != that.guid) { d3.event.stopPropagation(); } }) .on('click', function(d) { if (d3.event.defaultPrevented) return; // ignore drag 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(force.drag); // add the nodes var circleContainer = node.append("g"); circleContainer.on("dblclick", function(d) { if ((_.isArray(d.value) && d.value.length == 1) || d.value.guid) { var guid = _.isArray(d.value) ? _.first(d.value).guid : d.value.guid; Utils.setUrl({ url: '#!/detailPage/' + guid, mergeBrowserUrl: false, urlParams: { tabActive: 'relationship' }, trigger: true }); } }) 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 activeEntityColor; } } else if (isAllEntityRelationDeleted({ data: d, type: 'node' })) { return deletedEntityColor; } else { return defaultEntityColor; } }) .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) { if (d && d.value && d.value.guid == that.guid) { return "#fff"; } else if (isAllEntityRelationDeleted({ data: d, type: 'node' })) { return "#fff"; } else { return "#000"; } }); 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; } }); // add the text node.append("text") .attr("x", -15) .attr("y", "35") .text(function(d) { return d.name; }); // add the curvy lines function tick() { 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) { if (d && d.value && d.value.guid == that.guid) { d.x = (width / 2) d.y = (height / 2) } return "translate(" + d.x + "," + d.y + ")"; }); } function dragstart(d) { d3.select(this).classed("fixed", d.fixed = true); } }, createTable: function() { this.entityModel = new VEntity({}); var table = CommonViewFunction.propertyTable({ scope: this, valueObject: this.entity.relationshipAttributes, attributeDefs: this.attributeDefs }); this.ui.relationshipDetailValue.html(table); }, relationshipViewToggle: function(checked) { if (checked) { this.ui.relationshipDetailTable.show(); this.ui.relationshipSVG.hide(); this.ui.zoomControl.hide(); this.$el.addClass('auto-height'); } else { this.ui.relationshipDetailTable.hide(); this.ui.relationshipSVG.show(); this.ui.zoomControl.show(); this.$el.removeClass('auto-height'); } } }); return RelationshipLayoutView; });