| /* |
| * 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. |
| */ |
| /* global define, module, require, exports */ |
| |
| (function (root, factory) { |
| if (typeof define === 'function' && define.amd) { |
| define(['d3', |
| 'jquery', |
| 'nf.Common', |
| 'nf.ErrorHandler', |
| 'nf.Dialog', |
| 'nf.Clipboard', |
| 'nf.Storage'], |
| function (d3, $, nfCommon, nfErrorHandler, nfDialog, nfClipboard, nfStorage) { |
| return (nf.CanvasUtils = factory(d3, $, nfCommon, nfErrorHandler, nfDialog, nfClipboard, nfStorage)); |
| }); |
| } else if (typeof exports === 'object' && typeof module === 'object') { |
| module.exports = (nf.CanvasUtils = factory( |
| require('d3'), |
| require('jquery'), |
| require('nf.Common'), |
| require('nf.ErrorHandler'), |
| require('nf.Dialog'), |
| require('nf.Clipboard'), |
| require('nf.Storage'))); |
| } else { |
| nf.CanvasUtils = factory( |
| root.d3, |
| root.$, |
| root.nf.Common, |
| root.nf.ErrorHandler, |
| root.nf.Dialog, |
| root.nf.Clipboard, |
| root.nf.Storage); |
| } |
| }(this, function (d3, $, nfCommon, nfErrorHandler, nfDialog, nfClipboard, nfStorage) { |
| 'use strict'; |
| |
| var nfCanvas; |
| var nfActions; |
| var nfSnippet; |
| var nfBirdseye; |
| var nfGraph; |
| var trimLengthCaches = {}; |
| |
| var restrictedUsage = d3.map(); |
| var requiredPermissions = d3.map(); |
| |
| var config = { |
| storage: { |
| namePrefix: 'nifi-view-' |
| }, |
| urls: { |
| controller: '../nifi-api/controller' |
| } |
| }; |
| |
| var MAX_URL_LENGTH = 2000; // the maximum (suggested) safe string length of a URL supported by all browsers and application servers |
| |
| var TWO_PI = 2 * Math.PI; |
| |
| var binarySearch = function (length, comparator) { |
| var low = 0; |
| var high = length - 1; |
| var mid; |
| |
| var result = 0; |
| while (low <= high) { |
| mid = ~~((low + high) / 2); |
| result = comparator(mid); |
| if (result < 0) { |
| high = mid - 1; |
| } else if (result > 0) { |
| low = mid + 1; |
| } else { |
| break; |
| } |
| } |
| |
| return mid; |
| }; |
| |
| var moveComponents = function (components, groupId) { |
| return $.Deferred(function (deferred) { |
| var parentGroupId = nfCanvasUtils.getGroupId(); |
| |
| // create a snippet for the specified components |
| var snippet = nfSnippet.marshal(components, parentGroupId); |
| nfSnippet.create(snippet).done(function (response) { |
| // move the snippet into the target |
| nfSnippet.move(response.snippet.id, groupId).done(function () { |
| var componentMap = d3.map(); |
| |
| // add the id to the type's array |
| var addComponent = function (type, id) { |
| if (!componentMap.has(type)) { |
| componentMap.set(type, []); |
| } |
| componentMap.get(type).push(id); |
| }; |
| |
| // go through each component being removed |
| components.each(function (d) { |
| addComponent(d.type, d.id); |
| }); |
| |
| // refresh all component types as necessary (handle components that have been removed) |
| componentMap.each(function (ids, type) { |
| nfCanvasUtils.getComponentByType(type).remove(ids); |
| }); |
| |
| // refresh the birdseye |
| nfBirdseye.refresh(); |
| |
| deferred.resolve(); |
| }).fail(nfErrorHandler.handleAjaxError).fail(function () { |
| deferred.reject(); |
| }); |
| }).fail(nfErrorHandler.handleAjaxError).fail(function () { |
| deferred.reject(); |
| }); |
| }).promise(); |
| }; |
| |
| var nfCanvasUtils = { |
| |
| /** |
| * Initialize the canvas utils. |
| * |
| * @param nfCanvasRef The nfCanvas module. |
| * @param nfActionsRef The nfActions module. |
| * @param nfSnippetRef The nfSnippet module. |
| * @param nfBirdseyeRef The nfBirdseye module. |
| * @param nfGraphRef The nfGraph module. |
| */ |
| init: function(nfCanvasRef, nfActionsRef, nfSnippetRef, nfBirdseyeRef, nfGraphRef){ |
| nfCanvas = nfCanvasRef; |
| nfActions = nfActionsRef; |
| nfSnippet = nfSnippetRef; |
| nfBirdseye = nfBirdseyeRef; |
| nfGraph = nfGraphRef; |
| }, |
| |
| config: { |
| systemTooltipConfig: { |
| style: { |
| classes: 'nifi-tooltip' |
| }, |
| show: { |
| solo: true, |
| effect: false |
| }, |
| hide: { |
| effect: false |
| }, |
| position: { |
| at: 'bottom right', |
| my: 'top left' |
| } |
| } |
| }, |
| |
| /** |
| * Gets a graph component `type`. |
| * |
| * @param type The type of component. |
| */ |
| getComponentByType: function (type) { |
| return nfGraph.getComponentByType(type); |
| }, |
| |
| /** |
| * Calculates the point on the specified bounding box that is closest to the |
| * specified point. |
| * |
| * @param {object} p The point |
| * @param {object} bBox The bounding box |
| */ |
| getPerimeterPoint: function (p, bBox) { |
| // calculate theta |
| var theta = Math.atan2(bBox.height, bBox.width); |
| |
| // get the rectangle radius |
| var xRadius = bBox.width / 2; |
| var yRadius = bBox.height / 2; |
| |
| // get the center point |
| var cx = bBox.x + xRadius; |
| var cy = bBox.y + yRadius; |
| |
| // calculate alpha |
| var dx = p.x - cx; |
| var dy = p.y - cy; |
| var alpha = Math.atan2(dy, dx); |
| |
| // normalize aphla into 0 <= alpha < 2 PI |
| alpha = alpha % TWO_PI; |
| if (alpha < 0) { |
| alpha += TWO_PI; |
| } |
| |
| // calculate beta |
| var beta = (Math.PI / 2) - alpha; |
| |
| // detect the appropriate quadrant and return the point on the perimeter |
| if ((alpha >= 0 && alpha < theta) || (alpha >= (TWO_PI - theta) && alpha < TWO_PI)) { |
| // right quadrant |
| return { |
| 'x': bBox.x + bBox.width, |
| 'y': cy + Math.tan(alpha) * xRadius |
| }; |
| } else if (alpha >= theta && alpha < (Math.PI - theta)) { |
| // bottom quadrant |
| return { |
| 'x': cx + Math.tan(beta) * yRadius, |
| 'y': bBox.y + bBox.height |
| }; |
| } else if (alpha >= (Math.PI - theta) && alpha < (Math.PI + theta)) { |
| // left quadrant |
| return { |
| 'x': bBox.x, |
| 'y': cy - Math.tan(alpha) * xRadius |
| }; |
| } else { |
| // top quadrant |
| return { |
| 'x': cx - Math.tan(beta) * yRadius, |
| 'y': bBox.y |
| }; |
| } |
| }, |
| |
| /** |
| * Queries for bulletins for the specified components. |
| * |
| * @param {array} componentIds |
| * @returns {deferred} |
| */ |
| queryBulletins: function (componentIds) { |
| var queries = []; |
| |
| var query = function (ids) { |
| var url = new URL(window.location); |
| var origin = nfCommon.substringBeforeLast(url.href, '/nifi'); |
| var endpoint = origin + '/nifi-api/flow/bulletin-board?' + $.param({ |
| sourceId: ids.join('|') |
| }); |
| |
| if (endpoint.length > MAX_URL_LENGTH) { |
| // split into two arrays and recurse with both halves |
| var mid = Math.ceil(ids.length / 2); |
| |
| // left half |
| var left = ids.slice(0, mid); |
| if (left.length > 0) { |
| query(left); |
| } |
| |
| // right half |
| var right = ids.slice(mid); |
| if (right.length > 0) { |
| query(right); |
| } |
| } else { |
| queries.push($.ajax({ |
| type: 'GET', |
| url: endpoint, |
| dataType: 'json' |
| })); |
| } |
| }; |
| |
| // initiate the queries |
| query(componentIds); |
| |
| if (queries.length === 1) { |
| // if there was only one query, return it |
| return $.Deferred(function (deferred) { |
| queries[0].done(function (response) { |
| deferred.resolve(response); |
| }).fail(function () { |
| deferred.reject(); |
| }).fail(nfErrorHandler.handleAjaxError); |
| }).promise(); |
| } else { |
| // if there were multiple queries, wait for each to complete |
| return $.Deferred(function (deferred) { |
| $.when.apply(window, queries).done(function () { |
| var results = $.makeArray(arguments); |
| |
| var generated = null; |
| var bulletins = []; |
| |
| $.each(results, function (_, result) { |
| var response = result[0]; |
| var bulletinBoard = response.bulletinBoard; |
| |
| // use the first generated timestamp |
| if (generated === null) { |
| generated = bulletinBoard.generated; |
| } |
| |
| // build up all the bulletins |
| Array.prototype.push.apply(bulletins, bulletinBoard.bulletins); |
| }); |
| |
| // sort all the bulletins |
| bulletins.sort(function (a, b) { |
| return b.id - a.id; |
| }); |
| |
| // resolve with a aggregated result |
| deferred.resolve({ |
| bulletinBoard: { |
| generated: generated, |
| bulletins: bulletins |
| } |
| }); |
| }).fail(function () { |
| deferred.reject(); |
| }).fail(nfErrorHandler.handleAjaxError); |
| }).promise(); |
| } |
| }, |
| |
| /** |
| * Shows the specified component in the specified group. |
| * |
| * @param {string} groupId The id of the group |
| * @param {string} componentId The id of the component |
| */ |
| showComponent: function (groupId, componentId) { |
| // ensure the group id is specified |
| if (nfCommon.isDefinedAndNotNull(groupId)) { |
| // initiate a graph refresh |
| var refreshGraph = $.Deferred(function (deferred) { |
| // load a different group if necessary |
| if (groupId !== nfCanvas.getGroupId()) { |
| // load the process group |
| nfCanvas.reload({}, groupId).done(function () { |
| deferred.resolve(); |
| }).fail(function (xhr, status, error) { |
| nfDialog.showOkDialog({ |
| headerText: 'Error', |
| dialogContent: 'Unable to load the group for the specified component.' |
| }); |
| deferred.reject(xhr, status, error); |
| }); |
| } else { |
| deferred.resolve(); |
| } |
| }).promise(); |
| |
| // when the refresh has completed, select the match |
| refreshGraph.done(function () { |
| // attempt to locate the corresponding component |
| var component = d3.select('#id-' + componentId); |
| if (!component.empty()) { |
| nfActions.show(component); |
| } else { |
| nfDialog.showOkDialog({ |
| headerText: 'Error', |
| dialogContent: 'Unable to find the specified component.' |
| }); |
| } |
| }); |
| |
| return refreshGraph; |
| } else { |
| return $.Deferred(function (deferred) { |
| deferred.reject(); |
| }).promise(); |
| } |
| }, |
| |
| /** |
| * Displays the URL deep link on the canvas. |
| * |
| * @param forceCanvasLoad Boolean enabling the update of the URL parameters. |
| */ |
| showDeepLink: function (forceCanvasLoad) { |
| // deselect components |
| nfCanvasUtils.getSelection().classed('selected', false); |
| |
| // close the ok dialog if open |
| if ($('#nf-ok-dialog').is(':visible') === true) { |
| $('#nf-ok-dialog').modal('hide'); |
| } |
| |
| // Feature detection and browser support for URLSearchParams |
| if ('URLSearchParams' in window) { |
| // get the `urlSearchParams` from the URL |
| var urlSearchParams = new URL(window.location).searchParams; |
| // if the `urlSearchParams` are `undefined` then the browser does not support |
| // the URL object's `.searchParams` property |
| if (!nf.Common.isDefinedAndNotNull(urlSearchParams)) { |
| // attempt to get the `urlSearchParams` using the URLSearchParams constructor and |
| // the URL object's `.search` property |
| urlSearchParams = new URLSearchParams(new URL(window.location).search); |
| } |
| |
| var groupId = nfCanvasUtils.getGroupId(); |
| |
| // if the `urlSearchParams` are still `undefined` then the browser does not support |
| // the URL object's `.search` property. In this case we cannot support deep links. |
| if (nf.Common.isDefinedAndNotNull(urlSearchParams)) { |
| var componentIds = []; |
| |
| if (urlSearchParams.get('processGroupId')) { |
| groupId = urlSearchParams.get('processGroupId'); |
| } |
| if (urlSearchParams.get('componentIds')) { |
| componentIds = urlSearchParams.get('componentIds').split(','); |
| } |
| |
| // load the graph but do not update the browser history |
| if (componentIds.length >= 1) { |
| return nfCanvasUtils.showComponents(groupId, componentIds, forceCanvasLoad); |
| } else { |
| return nfCanvasUtils.getComponentByType('ProcessGroup').enterGroup(groupId); |
| } |
| } else { |
| return nfCanvasUtils.getComponentByType('ProcessGroup').enterGroup(groupId); |
| } |
| } |
| }, |
| |
| /** |
| * Shows the specified components in the specified group. |
| * |
| * @param {string} groupId The id of the group |
| * @param {array} componentIds The ids of the components |
| * @param {bool} forceCanvasLoad Boolean to force reload of the canvas. |
| */ |
| showComponents: function (groupId, componentIds, forceCanvasLoad) { |
| // ensure the group id is specified |
| if (nfCommon.isDefinedAndNotNull(groupId)) { |
| // initiate a graph refresh |
| var refreshGraph = $.Deferred(function (deferred) { |
| // load a different group if necessary |
| if (groupId !== nfCanvas.getGroupId() || forceCanvasLoad) { |
| // load the process group |
| nfCanvas.reload({}, groupId).done(function () { |
| deferred.resolve(); |
| }).fail(function (xhr, status, error) { |
| nfDialog.showOkDialog({ |
| headerText: 'Error', |
| dialogContent: 'Unable to enter the selected group.' |
| }); |
| |
| deferred.reject(xhr, status, error); |
| }); |
| } else { |
| deferred.resolve(); |
| } |
| }).promise(); |
| |
| // when the refresh has completed, select the match |
| refreshGraph.done(function () { |
| // get the components to select |
| var components = d3.selectAll('g.component, g.connection').filter(function (d) { |
| if (componentIds.indexOf(d.id) >= 0) { |
| // remove located components from array so that only unfound components will remain |
| componentIds.splice(componentIds.indexOf(d.id), 1); |
| return d; |
| } |
| }); |
| |
| if (componentIds.length > 0) { |
| var dialogContent = $('<p></p>').text('Specified component(s) not found: ' + componentIds.join(', ') + '.').append('<br/><br/>').append($('<p>Unable to select component(s).</p>')); |
| |
| nfDialog.showOkDialog({ |
| headerText: 'Error', |
| dialogContent: dialogContent |
| }); |
| } |
| |
| nfActions.show(components); |
| }); |
| |
| return refreshGraph; |
| } |
| }, |
| |
| /** |
| * Set the parameters of the URL. |
| * |
| * @param groupId The process group id. |
| * @param selections The component ids. |
| */ |
| setURLParameters: function (groupId, selections) { |
| // Feature detection and browser support for URLSearchParams |
| if ('URLSearchParams' in window) { |
| if (!nfCommon.isDefinedAndNotNull(groupId)) { |
| groupId = nfCanvasUtils.getGroupId(); |
| } |
| |
| if (!nfCommon.isDefinedAndNotNull(selections)) { |
| selections = nfCanvasUtils.getSelection(); |
| } |
| |
| var selectedComponentIds = []; |
| selections.each(function (selection) { |
| selectedComponentIds.push(selection.id); |
| }); |
| |
| // get all URL parameters |
| var url = new URL(window.location); |
| |
| // get the `params` from the URL |
| var params = new URL(window.location).searchParams; |
| // if the `params` are undefined then the browser does not support |
| // the URL object's `.searchParams` property |
| if (!nf.Common.isDefinedAndNotNull(params)) { |
| // attempt to get the `params` using the URLSearchParams constructor and |
| // the URL object's `.search` property |
| params = new URLSearchParams(url.search); |
| } |
| |
| // if the `params` are still `undefined` then the browser does not support |
| // the URL object's `.search` property. In this case we cannot support deep links. |
| if (nf.Common.isDefinedAndNotNull(params)) { |
| var params = new URLSearchParams(url.search); |
| params.set('processGroupId', groupId); |
| params.set('componentIds', selectedComponentIds.sort()); |
| |
| var newUrl = url.origin + url.pathname; |
| |
| if (nfCommon.isDefinedAndNotNull(nfCanvasUtils.getParentGroupId()) || selectedComponentIds.length > 0) { |
| if (!nfCommon.isDefinedAndNotNull(nfCanvasUtils.getParentGroupId())) { |
| // we are in the root group so set processGroupId param value to 'root' alias |
| params.set('processGroupId', 'root'); |
| } |
| |
| if ((url.origin + url.pathname + '?' + params.toString()).length <= MAX_URL_LENGTH) { |
| newUrl = url.origin + url.pathname + '?' + params.toString(); |
| } else if (nfCommon.isDefinedAndNotNull(nfCanvasUtils.getParentGroupId())) { |
| // silently remove all component ids |
| params.set('componentIds', ''); |
| newUrl = url.origin + url.pathname + '?' + params.toString(); |
| } |
| } |
| |
| window.history.replaceState({'previous_url': url.href}, window.document.title, newUrl); |
| } |
| } |
| }, |
| |
| /** |
| * Gets the currently selected components and connections. |
| * |
| * @returns {selection} The currently selected components and connections |
| */ |
| getSelection: function () { |
| return d3.selectAll('g.component.selected, g.connection.selected'); |
| }, |
| |
| /** |
| * Gets the selection object of the id passed. |
| * |
| * @param {id} The uuid of the component to retrieve |
| * @returns {selection} The selection object of the component id passed |
| */ |
| getSelectionById: function(id){ |
| return d3.select('#id-' + id); |
| }, |
| |
| /** |
| * Gets the coordinates neccessary to center a bounding box on the screen. |
| * |
| * @param {type} boundingBox |
| * @returns {number[]} |
| */ |
| getCenterForBoundingBox: function (boundingBox) { |
| var scale = nfCanvas.View.getScale(); |
| if (nfCommon.isDefinedAndNotNull(boundingBox.scale)) { |
| scale = boundingBox.scale; |
| } |
| |
| // get the canvas normalized width and height |
| var canvasContainer = $('#canvas-container'); |
| var screenWidth = canvasContainer.width() / scale; |
| var screenHeight = canvasContainer.height() / scale; |
| |
| // determine the center location for this component in canvas space |
| var center = [(screenWidth / 2) - (boundingBox.width / 2), (screenHeight / 2) - (boundingBox.height / 2)]; |
| return center; |
| }, |
| |
| /** |
| * Determines if a bounding box is fully in the current viewable canvas area. |
| * |
| * @param {type} boundingBox Bounding box to check. |
| * @param {boolean} strict If true, the entire bounding box must be in the viewport. |
| * If false, only part of the bounding box must be in the viewport. |
| * @returns {boolean} |
| */ |
| isBoundingBoxInViewport: function (boundingBox, strict) { |
| var scale = nfCanvas.View.getScale(); |
| var translate = nfCanvas.View.getTranslate(); |
| var offset = nfCanvas.CANVAS_OFFSET; |
| |
| // get the canvas normalized width and height |
| var canvasContainer = $('#canvas-container'); |
| var screenWidth = Math.floor(canvasContainer.width() / scale); |
| var screenHeight = Math.floor(canvasContainer.height() / scale); |
| var screenLeft = Math.ceil(-translate[0] / scale); |
| var screenTop = Math.ceil(-translate[1] / scale); |
| var screenRight = screenLeft + screenWidth; |
| var screenBottom = screenTop + screenHeight; |
| |
| var left = Math.ceil(boundingBox.x); |
| var right = Math.floor(boundingBox.x + boundingBox.width); |
| var top = Math.ceil(boundingBox.y - (offset) / scale); |
| var bottom = Math.floor(boundingBox.y - (offset / scale) + boundingBox.height); |
| |
| if (strict) { |
| return !(left < screenLeft || right > screenRight || top < screenTop || bottom > screenBottom); |
| } else { |
| return ((left > screenLeft && left < screenRight) || (right < screenRight && right > screenLeft)) && |
| ((top > screenTop && top < screenBottom) || (bottom < screenBottom && bottom > screenTop)); |
| } |
| }, |
| |
| /** |
| * Centers the specified bounding box. |
| * |
| * @param {type} boundingBox |
| */ |
| centerBoundingBox: function (boundingBox) { |
| var scale = nfCanvas.View.getScale(); |
| if (nfCommon.isDefinedAndNotNull(boundingBox.scale)) { |
| scale = boundingBox.scale; |
| } |
| |
| var center = nfCanvasUtils.getCenterForBoundingBox(boundingBox); |
| |
| // calculate the difference between the center point and the position of this component and convert to screen space |
| nfCanvas.View.transform([(center[0] - boundingBox.x) * scale, (center[1] - boundingBox.y) * scale], scale); |
| }, |
| |
| /** |
| * Enables/disables the editable behavior for the specified selection based on their access policies. |
| * |
| * @param selection selection |
| * @param nfConnectableRef The nfConnectable module. |
| * @param nfDraggableRef The nfDraggable module. |
| */ |
| editable: function (selection, nfConnectableRef, nfDraggableRef) { |
| if (nfCanvasUtils.canModify(selection)) { |
| if (!selection.classed('connectable')) { |
| selection.call(nfConnectableRef.activate); |
| } |
| if (!selection.classed('moveable')) { |
| selection.call(nfDraggableRef.activate); |
| } |
| } else { |
| if (selection.classed('connectable')) { |
| selection.call(nfConnectableRef.deactivate); |
| } |
| if (selection.classed('moveable')) { |
| selection.call(nfDraggableRef.deactivate); |
| } |
| } |
| }, |
| |
| /** |
| * Conditionally apply the transition. |
| * |
| * @param selection selection |
| * @param transition transition |
| */ |
| transition: function (selection, transition) { |
| if (transition && !selection.empty()) { |
| return selection.transition().duration(400); |
| } else { |
| return selection; |
| } |
| }, |
| |
| /** |
| * Position the component accordingly. |
| * |
| * @param {selection} updated |
| */ |
| position: function (updated, transition) { |
| if (updated.empty()) { |
| return; |
| } |
| |
| return nfCanvasUtils.transition(updated, transition) |
| .attr('transform', function (d) { |
| return 'translate(' + d.position.x + ', ' + d.position.y + ')'; |
| }); |
| }, |
| |
| /** |
| * Clears the cache used to avoid calculating whether or not ellipses are needed for a given text element |
| */ |
| clearEllipsisCache: function () { |
| trimLengthCaches = {}; |
| }, |
| |
| /** |
| * Applies single line ellipsis to the component in the specified selection if necessary. |
| * |
| * @param {selection} selection |
| * @param {string} text |
| * @param {cacheName} string |
| */ |
| ellipsis: function (selection, text, cacheName) { |
| text = text.trim(); |
| var width = parseInt(selection.attr('width'), 10); |
| var node = selection.node(); |
| |
| // set the element text |
| selection.text(text); |
| |
| // Never apply ellipses to text less than 5 characters and don't keep it in the cache |
| // because it could take up a lot of space unnecessarily. |
| var textLength = text.length; |
| if (textLength < 5) { |
| return; |
| } |
| |
| // Check our cache of text lengths to see if we already know how much to trim it to |
| var trimLengths = trimLengthCaches[cacheName]; |
| if (trimLengths === undefined) { |
| trimLengths = {}; |
| trimLengthCaches[cacheName] = trimLengths; |
| } |
| |
| var cacheForText = trimLengths[text]; |
| var trimLength = (cacheForText === undefined) ? undefined : cacheForText[width]; |
| if (trimLength === undefined) { |
| // We haven't cached the length for this text yet. Determine whether we need |
| // to trim & add ellipses or not |
| if (node.getSubStringLength(0, text.length - 1) > width) { |
| // make some room for the ellipsis |
| width -= 5; |
| |
| // determine the appropriate index |
| var i = binarySearch(text.length, function (x) { |
| var length = node.getSubStringLength(0, x); |
| if (length > width) { |
| // length is too long, try the lower half |
| return -1; |
| } else if (length < width) { |
| // length is too short, try the upper half |
| return 1; |
| } |
| return 0; |
| }); |
| |
| trimLength = i; |
| } else { |
| // trimLength of -1 indicates we do not need ellipses |
| trimLength = -1; |
| } |
| |
| // TODO: Can we clear this when process group changes? |
| // Store the trim length in our cache |
| if (trimLengths[text] === undefined) { |
| trimLengths[text] = {}; |
| } |
| trimLengths[text][width] = trimLength; |
| } |
| |
| if (trimLength === -1) { |
| return; |
| } |
| |
| // trim at the appropriate length and add ellipsis |
| selection.text(text.substring(0, trimLength) + String.fromCharCode(8230)); |
| }, |
| |
| /** |
| * Applies multiline ellipsis to the component in the specified seleciton. Text will |
| * wrap for the specified number of lines. The last line will be ellipsis if necessary. |
| * |
| * @param {selection} selection |
| * @param {integer} lineCount |
| * @param {string} text |
| * @param {string} cacheName |
| */ |
| multilineEllipsis: function (selection, lineCount, text, cacheName) { |
| var i = 1; |
| var words = text.split(/\s+/).reverse(); |
| |
| // get the appropriate position |
| var x = parseInt(selection.attr('x'), 10); |
| var y = parseInt(selection.attr('y'), 10); |
| var width = parseInt(selection.attr('width'), 10); |
| |
| var line = []; |
| var tspan = selection.append('tspan') |
| .attrs({ |
| 'x': x, |
| 'y': y, |
| 'width': width |
| }); |
| |
| // go through each word |
| var word = words.pop(); |
| while (nfCommon.isDefinedAndNotNull(word)) { |
| // add the current word |
| line.push(word); |
| |
| // update the label text |
| tspan.text(line.join(' ')); |
| |
| // if this word caused us to go too far |
| if (tspan.node().getComputedTextLength() > width) { |
| // remove the current word |
| line.pop(); |
| |
| // update the label text |
| tspan.text(line.join(' ')); |
| |
| // create the tspan for the next line |
| tspan = selection.append('tspan') |
| .attrs({ |
| 'x': x, |
| 'dy': '1.2em', |
| 'width': width |
| }); |
| |
| // if we've reached the last line, use single line ellipsis |
| if (++i >= lineCount) { |
| // get the remainder using the current word and |
| // reversing whats left |
| var remainder = [word].concat(words.reverse()); |
| |
| // apply ellipsis to the last line |
| nfCanvasUtils.ellipsis(tspan, remainder.join(' '), cacheName); |
| |
| // we've reached the line count |
| break; |
| } else { |
| tspan.text(word); |
| |
| // prep the line for the next iteration |
| line = [word]; |
| } |
| } |
| |
| // get the next word |
| word = words.pop(); |
| } |
| }, |
| |
| /** |
| * Updates the active thread count on the specified selection. |
| * |
| * @param {selection} selection The selection |
| * @param {object} d The data |
| * @param {function} setOffset Optional function to handle the width of the active thread count component |
| * @return |
| */ |
| activeThreadCount: function (selection, d, setOffset) { |
| var activeThreads = d.status.aggregateSnapshot.activeThreadCount; |
| var terminatedThreads = d.status.aggregateSnapshot.terminatedThreadCount; |
| |
| // if there is active threads show the count, otherwise hide |
| if (activeThreads > 0 || terminatedThreads > 0) { |
| var generateThreadsTip = function () { |
| var tip = activeThreads + ' active threads'; |
| if (terminatedThreads > 0) { |
| tip += ' (' + terminatedThreads + ' terminated)'; |
| } |
| |
| return tip; |
| }; |
| |
| // update the active thread count |
| var activeThreadCount = selection.select('text.active-thread-count') |
| .text(function () { |
| if (terminatedThreads > 0) { |
| return activeThreads + ' (' + terminatedThreads + ')'; |
| } else { |
| return activeThreads; |
| } |
| }) |
| .style('display', 'block') |
| .each(function () { |
| var activeThreadCountText = d3.select(this); |
| |
| var bBox = this.getBBox(); |
| activeThreadCountText.attr('x', function () { |
| return d.dimensions.width - bBox.width - 15; |
| }); |
| |
| // reset the active thread count tooltip |
| activeThreadCountText.selectAll('title').remove(); |
| }); |
| |
| // append the tooltip |
| activeThreadCount.append('title').text(generateThreadsTip); |
| |
| // update the background width |
| selection.select('text.active-thread-count-icon') |
| .attr('x', function () { |
| var bBox = activeThreadCount.node().getBBox(); |
| |
| // update the offset |
| if (typeof setOffset === 'function') { |
| setOffset(bBox.width + 6); |
| } |
| |
| return d.dimensions.width - bBox.width - 20; |
| }) |
| .style('fill', function () { |
| if (terminatedThreads > 0) { |
| return '#ba554a'; |
| } else { |
| return '#728e9b'; |
| } |
| }) |
| .style('display', 'block') |
| .each(function () { |
| var activeThreadCountIcon = d3.select(this); |
| |
| // reset the active thread count tooltip |
| activeThreadCountIcon.selectAll('title').remove(); |
| }).append('title').text(generateThreadsTip); |
| } else { |
| selection.selectAll('text.active-thread-count, text.active-thread-count-icon') |
| .style('display', 'none') |
| .each(function () { |
| d3.select(this).selectAll('title').remove(); |
| }); |
| } |
| }, |
| |
| /** |
| * Disables the default browser behavior of following image href when control clicking. |
| * |
| * @param {selection} selection The image |
| */ |
| disableImageHref: function (selection) { |
| selection.on('click.disableImageHref', function () { |
| if (d3.event.ctrlKey || d3.event.shiftKey) { |
| d3.event.preventDefault(); |
| } |
| }); |
| }, |
| |
| /** |
| * Handles component bulletins. |
| * |
| * @param {selection} selection The component |
| * @param {object} d The data |
| * @param {function} getTooltipContainer Function to get the tooltip container |
| * @param {function} offset Optional offset |
| */ |
| bulletins: function (selection, d, getTooltipContainer, offset) { |
| offset = nfCommon.isDefinedAndNotNull(offset) ? offset : 0; |
| |
| // get the tip |
| var tip = d3.select('#bulletin-tip-' + d.id); |
| |
| var hasBulletins = false; |
| if (!nfCommon.isEmpty(d.bulletins)) { |
| // format the bulletins |
| var bulletins = nfCommon.getFormattedBulletins(d.bulletins); |
| hasBulletins = bulletins.length > 0; |
| |
| if (hasBulletins) { |
| // create the unordered list based off the formatted bulletins |
| var list = nfCommon.formatUnorderedList(bulletins); |
| } |
| } |
| |
| // if there are bulletins show them, otherwise hide |
| if (hasBulletins) { |
| // update the tooltip |
| selection.select('text.bulletin-icon') |
| .each(function () { |
| // create the tip if necessary |
| if (tip.empty()) { |
| tip = getTooltipContainer().append('div') |
| .attr('id', function () { |
| return 'bulletin-tip-' + d.id; |
| }) |
| .attr('class', 'tooltip nifi-tooltip'); |
| } |
| |
| // add the tooltip |
| tip.html(function () { |
| return $('<div></div>').append(list).html(); |
| }); |
| |
| nfCanvasUtils.canvasTooltip(tip, d3.select(this)); |
| }); |
| |
| // update the tooltip background |
| selection.select('text.bulletin-icon').style("visibility", "visible"); |
| selection.select('rect.bulletin-background').style("visibility", "visible"); |
| } else { |
| // clean up if necessary |
| if (!tip.empty()) { |
| tip.remove(); |
| } |
| |
| // update the tooltip background |
| selection.select('text.bulletin-icon').style("visibility", "hidden"); |
| selection.select('rect.bulletin-background').style("visibility", "hidden"); |
| } |
| }, |
| |
| /** |
| * Adds the specified tooltip to the specified target. |
| * |
| * @param {selection} tip The tooltip |
| * @param {selection} target The target of the tooltip |
| */ |
| canvasTooltip: function (tip, target) { |
| target.on('mouseenter', function () { |
| tip.style('top', (d3.event.pageY + 15) + 'px').style('left', (d3.event.pageX + 15) + 'px').style('display', 'block'); |
| }) |
| .on('mousemove', function () { |
| tip.style('top', (d3.event.pageY + 15) + 'px').style('left', (d3.event.pageX + 15) + 'px'); |
| }) |
| .on('mouseleave', function () { |
| tip.style('display', 'none'); |
| }); |
| }, |
| |
| /** |
| * Determines if the specified selection is alignable (in a single action). |
| * |
| * @param {selection} selection The selection |
| * @returns {boolean} |
| */ |
| canAlign: function(selection) { |
| var canAlign = true; |
| |
| // determine if the current selection is entirely connections |
| var selectedConnections = selection.filter(function(d) { |
| var connection = d3.select(this); |
| return nfCanvasUtils.isConnection(connection); |
| }); |
| |
| // require multiple selections besides connections |
| if (selection.size() - selectedConnections.size() < 2) { |
| canAlign = false; |
| } |
| |
| // require write permissions |
| if (nfCanvasUtils.canModify(selection) === false) { |
| canAlign = false; |
| } |
| |
| return canAlign; |
| }, |
| |
| /** |
| * Determines if the specified selection is colorable (in a single action). |
| * |
| * @param {selection} selection The selection |
| * @returns {boolean} |
| */ |
| isColorable: function(selection) { |
| if (selection.empty()) { |
| return false; |
| } |
| |
| // require read and write permissions |
| if (nfCanvasUtils.canRead(selection) === false || nfCanvasUtils.canModify(selection) === false) { |
| return false; |
| } |
| |
| // determine if the current selection is entirely processors or labels |
| var selectedProcessors = selection.filter(function(d) { |
| var processor = d3.select(this); |
| return nfCanvasUtils.isProcessor(processor) && nfCanvasUtils.canModify(processor); |
| }); |
| var selectedLabels = selection.filter(function(d) { |
| var label = d3.select(this); |
| return nfCanvasUtils.isLabel(label) && nfCanvasUtils.canModify(label); |
| }); |
| |
| var allProcessors = selectedProcessors.size() === selection.size(); |
| var allLabels = selectedLabels.size() === selection.size(); |
| |
| return allProcessors || allLabels; |
| }, |
| |
| /** |
| * Determines if the specified selection is a connection. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isConnection: function (selection) { |
| return selection.classed('connection'); |
| }, |
| |
| /** |
| * Determines if the specified selection is a remote process group. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isRemoteProcessGroup: function (selection) { |
| return selection.classed('remote-process-group'); |
| }, |
| |
| /** |
| * Determines if the specified selection is a processor. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isProcessor: function (selection) { |
| return selection.classed('processor'); |
| }, |
| |
| /** |
| * Determines if the specified selection is a label. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isLabel: function (selection) { |
| return selection.classed('label'); |
| }, |
| |
| /** |
| * Determines if the specified selection is an input port. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isInputPort: function (selection) { |
| return selection.classed('input-port'); |
| }, |
| |
| /** |
| * Determines if the specified selection is an output port. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isOutputPort: function (selection) { |
| return selection.classed('output-port'); |
| }, |
| |
| /** |
| * Determines if the specified selection is a process group. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isProcessGroup: function (selection) { |
| return selection.classed('process-group'); |
| }, |
| |
| /** |
| * Determines if the specified selection is a funnel. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isFunnel: function (selection) { |
| return selection.classed('funnel'); |
| }, |
| |
| /** |
| * Determines if the components in the specified selection are runnable. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection is runnable |
| */ |
| areRunnable: function (selection) { |
| if (selection.empty()) { |
| return true; |
| } |
| |
| var runnable = true; |
| selection.each(function () { |
| if (!nfCanvasUtils.isRunnable(d3.select(this))) { |
| runnable = false; |
| return false; |
| } |
| }); |
| |
| return runnable; |
| }, |
| |
| /** |
| * Determines if the component in the specified selection is runnable. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection is runnable |
| */ |
| isRunnable: function (selection) { |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| if (nfCanvasUtils.isProcessGroup(selection)) { |
| return true; |
| } |
| |
| if (nfCanvasUtils.canOperate(selection) === false) { |
| return false; |
| } |
| |
| var runnable = false; |
| var selectionData = selection.datum(); |
| if (nfCanvasUtils.isProcessor(selection) || nfCanvasUtils.isInputPort(selection) || nfCanvasUtils.isOutputPort(selection)) { |
| runnable = nfCanvasUtils.supportsModification(selection) && selectionData.status.aggregateSnapshot.runStatus === 'Stopped'; |
| } |
| |
| return runnable; |
| }, |
| |
| /** |
| * Determines if the components in the specified selection are stoppable. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection is stoppable |
| */ |
| areStoppable: function (selection) { |
| if (selection.empty()) { |
| return true; |
| } |
| |
| var stoppable = true; |
| selection.each(function () { |
| if (!nfCanvasUtils.isStoppable(d3.select(this))) { |
| stoppable = false; |
| return false; |
| } |
| }); |
| |
| return stoppable; |
| }, |
| |
| /** |
| * Determines if the component in the specified selection is runnable. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection is runnable |
| */ |
| isStoppable: function (selection) { |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| if (nfCanvasUtils.isProcessGroup(selection)) { |
| return true; |
| } |
| |
| if (nfCanvasUtils.canOperate(selection) === false) { |
| return false; |
| } |
| |
| var stoppable = false; |
| var selectionData = selection.datum(); |
| if (nfCanvasUtils.isProcessor(selection) || nfCanvasUtils.isInputPort(selection) || nfCanvasUtils.isOutputPort(selection)) { |
| stoppable = selectionData.status.aggregateSnapshot.runStatus === 'Running'; |
| } |
| |
| return stoppable; |
| }, |
| |
| /** |
| * Filters the specified selection for any components that supports enable. |
| * |
| * @argument {selection} selection The selection |
| */ |
| filterEnable: function (selection) { |
| return selection.filter(function (d) { |
| var selected = d3.select(this); |
| var selectedData = selected.datum(); |
| |
| // enable always allowed for PGs since they will invoke the /flow endpoint for enabling all applicable components (based on permissions) |
| if (nfCanvasUtils.isProcessGroup(selected)) { |
| return true; |
| } |
| |
| // not a PG, verify permissions to modify |
| if (nfCanvasUtils.canOperate(selected) === false) { |
| return false; |
| } |
| |
| // ensure its a processor, input port, or output port and supports modification and is disabled (can enable) |
| return ((nfCanvasUtils.isProcessor(selected) || nfCanvasUtils.isInputPort(selected) || nfCanvasUtils.isOutputPort(selected)) && |
| nfCanvasUtils.supportsModification(selected) && selectedData.status.aggregateSnapshot.runStatus === 'Disabled'); |
| }); |
| }, |
| |
| /** |
| * Determines if the specified selection contains any components that supports enable. |
| * |
| * @argument {selection} selection The selection |
| */ |
| canEnable: function (selection) { |
| if (selection.empty()) { |
| return true; |
| } |
| |
| return nfCanvasUtils.filterEnable(selection).size() === selection.size(); |
| }, |
| |
| /** |
| * Filters the specified selection for any components that supports disable. |
| * |
| * @argument {selection} selection The selection |
| */ |
| filterDisable: function (selection) { |
| return selection.filter(function (d) { |
| var selected = d3.select(this); |
| var selectedData = selected.datum(); |
| |
| // disable always allowed for PGs since they will invoke the /flow endpoint for disabling all applicable components (based on permissions) |
| if (nfCanvasUtils.isProcessGroup(selected)) { |
| return true; |
| } |
| |
| // not a PG, verify permissions to modify |
| if (nfCanvasUtils.canOperate(selected) === false) { |
| return false; |
| } |
| |
| // ensure its a processor, input port, or output port and supports modification and is stopped (can disable) |
| return ((nfCanvasUtils.isProcessor(selected) || nfCanvasUtils.isInputPort(selected) || nfCanvasUtils.isOutputPort(selected)) && |
| nfCanvasUtils.supportsModification(selected) && |
| (selectedData.status.aggregateSnapshot.runStatus === 'Stopped' || selectedData.status.aggregateSnapshot.runStatus === 'Invalid')); |
| }); |
| }, |
| |
| /** |
| * Determines if the specified selection contains any components that supports disable. |
| * |
| * @argument {selection} selection The selection |
| */ |
| canDisable: function (selection) { |
| if (selection.empty()) { |
| return true; |
| } |
| |
| return nfCanvasUtils.filterDisable(selection).size() === selection.size(); |
| }, |
| |
| |
| /** |
| * Determines if the specified selection can all start transmitting. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection can start transmitting |
| */ |
| canAllStartTransmitting: function (selection) { |
| if (selection.empty()) { |
| return false; |
| } |
| |
| var canStartTransmitting = true; |
| selection.each(function () { |
| if (!nfCanvasUtils.canStartTransmitting(d3.select(this))) { |
| canStartTransmitting = false; |
| } |
| }); |
| return canStartTransmitting; |
| }, |
| |
| /** |
| * Determines if the specified selection supports starting transmission. |
| * |
| * @argument {selection} selection The selection |
| */ |
| canStartTransmitting: function (selection) { |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| if ((nfCanvasUtils.canModify(selection) === false || nfCanvasUtils.canRead(selection) === false) |
| && nfCanvasUtils.canOperate(selection) === false) { |
| return false; |
| } |
| |
| return nfCanvasUtils.isRemoteProcessGroup(selection); |
| }, |
| |
| /** |
| * Determines if the specified selection can all stop transmitting. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection can stop transmitting |
| */ |
| canAllStopTransmitting: function (selection) { |
| if (selection.empty()) { |
| return false; |
| } |
| |
| var canStopTransmitting = true; |
| selection.each(function () { |
| if (!nfCanvasUtils.canStopTransmitting(d3.select(this))) { |
| canStopTransmitting = false; |
| } |
| }); |
| return canStopTransmitting; |
| }, |
| |
| /** |
| * Determines if the specified selection can stop transmission. |
| * |
| * @argument {selection} selection The selection |
| */ |
| canStopTransmitting: function (selection) { |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| if ((nfCanvasUtils.canModify(selection) === false || nfCanvasUtils.canRead(selection) === false) |
| && nfCanvasUtils.canOperate(selection) === false) { |
| return false; |
| } |
| |
| return nfCanvasUtils.isRemoteProcessGroup(selection); |
| }, |
| |
| /** |
| * Determines whether the components in the specified selection are deletable. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection is deletable |
| */ |
| areDeletable: function (selection) { |
| if (selection.empty()) { |
| return false; |
| } |
| |
| var isDeletable = true; |
| selection.each(function () { |
| if (!nfCanvasUtils.isDeletable(d3.select(this))) { |
| isDeletable = false; |
| } |
| }); |
| return isDeletable; |
| }, |
| |
| /** |
| * Determines whether the component in the specified selection is deletable. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection is deletable |
| */ |
| isDeletable: function (selection) { |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| // ensure the user has write permissions to the current process group |
| if (nfCanvas.canWrite() === false) { |
| return false; |
| } |
| |
| if (nfCanvasUtils.canModify(selection) === false) { |
| return false; |
| } |
| |
| return nfCanvasUtils.supportsModification(selection); |
| }, |
| |
| /** |
| * Determines whether the specified selection is configurable. |
| * |
| * @param selection |
| */ |
| isConfigurable: function (selection) { |
| // ensure the correct number of components are selected |
| if (selection.size() !== 1) { |
| if (selection.empty()) { |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| if (nfCanvasUtils.isProcessGroup(selection)) { |
| return true; |
| } |
| if (nfCanvasUtils.canRead(selection) === false || nfCanvasUtils.canModify(selection) === false) { |
| return false; |
| } |
| if (nfCanvasUtils.isFunnel(selection)) { |
| return false; |
| } |
| |
| return nfCanvasUtils.supportsModification(selection); |
| }, |
| |
| /** |
| * Determines whether the specified selection has details. |
| * |
| * @param selection |
| */ |
| hasDetails: function (selection) { |
| // ensure the correct number of components are selected |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| if (nfCanvasUtils.canRead(selection) === false) { |
| return false; |
| } |
| if (nfCanvasUtils.canModify(selection)) { |
| if (nfCanvasUtils.isProcessor(selection) || nfCanvasUtils.isInputPort(selection) || nfCanvasUtils.isOutputPort(selection) || nfCanvasUtils.isRemoteProcessGroup(selection) || nfCanvasUtils.isConnection(selection)) { |
| return !nfCanvasUtils.isConfigurable(selection); |
| } |
| } else { |
| return nfCanvasUtils.isProcessor(selection) || nfCanvasUtils.isConnection(selection) || nfCanvasUtils.isInputPort(selection) || nfCanvasUtils.isOutputPort(selection) || nfCanvasUtils.isRemoteProcessGroup(selection); |
| } |
| |
| return false; |
| }, |
| |
| /** |
| * Determines whether the user can configure or open the policy management page. |
| */ |
| canManagePolicies: function () { |
| var selection = nfCanvasUtils.getSelection(); |
| |
| // ensure 0 or 1 components selected |
| if (selection.size() <= 1) { |
| // if something is selected, ensure it's not a connection |
| if (!selection.empty() && nfCanvasUtils.isConnection(selection)) { |
| return false; |
| } |
| |
| // ensure access to read tenants |
| return nfCommon.canAccessTenants(); |
| } |
| |
| return false; |
| }, |
| |
| /** |
| * Determines whether the components in the specified selection are writable. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection is writable |
| */ |
| canModify: function (selection) { |
| var selectionSize = selection.size(); |
| var writableSize = selection.filter(function (d) { |
| return d.permissions.canWrite; |
| }).size(); |
| |
| return selectionSize === writableSize; |
| }, |
| |
| /** |
| * Determines whether the components in the specified selection are readable. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection is readable |
| */ |
| canRead: function (selection) { |
| var selectionSize = selection.size(); |
| var readableSize = selection.filter(function (d) { |
| return d.permissions.canRead; |
| }).size(); |
| |
| return selectionSize === readableSize; |
| }, |
| |
| /** |
| * Determines whether the components in the specified selection can be operated. |
| * |
| * @argument {selection} selection The selection |
| * @return {boolean} Whether the selection can be operated |
| */ |
| canOperate: function (selection) { |
| var selectionSize = selection.size(); |
| var writableSize = selection.filter(function (d) { |
| return d.permissions.canWrite || (d.operatePermissions && d.operatePermissions.canWrite); |
| }).size(); |
| |
| return selectionSize === writableSize; |
| }, |
| |
| /** |
| * Determines whether the specified selection is in a state to support modification. |
| * |
| * @argument {selection} selection The selection |
| */ |
| supportsModification: function (selection) { |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| // get the selection data |
| var selectionData = selection.datum(); |
| |
| var supportsModification = false; |
| if (nfCanvasUtils.isProcessor(selection) || nfCanvasUtils.isInputPort(selection) || nfCanvasUtils.isOutputPort(selection)) { |
| supportsModification = !(selectionData.status.aggregateSnapshot.runStatus === 'Running' || selectionData.status.aggregateSnapshot.activeThreadCount > 0); |
| } else if (nfCanvasUtils.isRemoteProcessGroup(selection)) { |
| supportsModification = !(selectionData.status.transmissionStatus === 'Transmitting' || selectionData.status.aggregateSnapshot.activeThreadCount > 0); |
| } else if (nfCanvasUtils.isProcessGroup(selection)) { |
| supportsModification = true; |
| } else if (nfCanvasUtils.isFunnel(selection)) { |
| supportsModification = true; |
| } else if (nfCanvasUtils.isLabel(selection)) { |
| supportsModification = true; |
| } else if (nfCanvasUtils.isConnection(selection)) { |
| var isSourceConfigurable = false; |
| var isDestinationConfigurable = false; |
| |
| var sourceComponentId = nfCanvasUtils.getConnectionSourceComponentId(selectionData); |
| var source = d3.select('#id-' + sourceComponentId); |
| if (!source.empty()) { |
| if (nfCanvasUtils.isRemoteProcessGroup(source) || nfCanvasUtils.isProcessGroup(source)) { |
| isSourceConfigurable = true; |
| } else { |
| isSourceConfigurable = nfCanvasUtils.supportsModification(source); |
| } |
| } |
| |
| var destinationComponentId = nfCanvasUtils.getConnectionDestinationComponentId(selectionData); |
| var destination = d3.select('#id-' + destinationComponentId); |
| if (!destination.empty()) { |
| if (nfCanvasUtils.isRemoteProcessGroup(destination) || nfCanvasUtils.isProcessGroup(destination)) { |
| isDestinationConfigurable = true; |
| } else { |
| isDestinationConfigurable = nfCanvasUtils.supportsModification(destination); |
| } |
| } |
| |
| supportsModification = isSourceConfigurable && isDestinationConfigurable; |
| } |
| return supportsModification; |
| }, |
| |
| /** |
| * Determines the connectable type for the specified source selection. |
| * |
| * @argument {selection} selection The selection |
| */ |
| getConnectableTypeForSource: function (selection) { |
| var type; |
| if (nfCanvasUtils.isProcessor(selection)) { |
| type = 'PROCESSOR'; |
| } else if (nfCanvasUtils.isRemoteProcessGroup(selection)) { |
| type = 'REMOTE_OUTPUT_PORT'; |
| } else if (nfCanvasUtils.isProcessGroup(selection)) { |
| type = 'OUTPUT_PORT'; |
| } else if (nfCanvasUtils.isInputPort(selection)) { |
| type = 'INPUT_PORT'; |
| } else if (nfCanvasUtils.isFunnel(selection)) { |
| type = 'FUNNEL'; |
| } |
| return type; |
| }, |
| |
| /** |
| * Determines the connectable type for the specified destination selection. |
| * |
| * @argument {selection} selection The selection |
| */ |
| getConnectableTypeForDestination: function (selection) { |
| var type; |
| if (nfCanvasUtils.isProcessor(selection)) { |
| type = 'PROCESSOR'; |
| } else if (nfCanvasUtils.isRemoteProcessGroup(selection)) { |
| type = 'REMOTE_INPUT_PORT'; |
| } else if (nfCanvasUtils.isProcessGroup(selection)) { |
| type = 'INPUT_PORT'; |
| } else if (nfCanvasUtils.isOutputPort(selection)) { |
| type = 'OUTPUT_PORT'; |
| } else if (nfCanvasUtils.isFunnel(selection)) { |
| type = 'FUNNEL'; |
| } |
| return type; |
| }, |
| |
| /** |
| * Determines if the graph is currently in a state to copy. |
| * |
| * @argument {selection} selection The selection |
| */ |
| isCopyable: function (selection) { |
| // if nothing is selected return |
| if (selection.empty()) { |
| return false; |
| } |
| |
| if (nfCanvasUtils.canRead(selection) === false) { |
| return false; |
| } |
| |
| // determine how many copyable components are selected |
| var copyable = selection.filter(function (d) { |
| var selected = d3.select(this); |
| if (nfCanvasUtils.isConnection(selected)) { |
| var sourceIncluded = !selection.filter(function (source) { |
| var sourceComponentId = nfCanvasUtils.getConnectionSourceComponentId(d); |
| return sourceComponentId === source.id; |
| }).empty(); |
| var destinationIncluded = !selection.filter(function (destination) { |
| var destinationComponentId = nfCanvasUtils.getConnectionDestinationComponentId(d); |
| return destinationComponentId === destination.id; |
| }).empty(); |
| return sourceIncluded && destinationIncluded; |
| } else { |
| return nfCanvasUtils.isProcessor(selected) || nfCanvasUtils.isFunnel(selected) || nfCanvasUtils.isLabel(selected) || nfCanvasUtils.isProcessGroup(selected) || nfCanvasUtils.isRemoteProcessGroup(selected) || nfCanvasUtils.isInputPort(selected) || nfCanvasUtils.isOutputPort(selected); |
| } |
| }); |
| |
| // ensure everything selected is copyable |
| return selection.size() === copyable.size(); |
| }, |
| |
| /** |
| * Determines if something is currently pastable. |
| */ |
| isPastable: function () { |
| return nfCanvas.canWrite() && nfClipboard.isCopied(); |
| }, |
| |
| /** |
| * Persists the current user view. |
| */ |
| persistUserView: function () { |
| var name = config.storage.namePrefix + nfCanvas.getGroupId(); |
| |
| // create the item to store |
| var translate = nfCanvas.View.getTranslate(); |
| var item = { |
| scale: nfCanvas.View.getScale(), |
| translateX: translate[0], |
| translateY: translate[1] |
| }; |
| |
| // store the item |
| nfStorage.setItem(name, item); |
| }, |
| |
| /** |
| * Gets the name for this connection. |
| * |
| * @param {object} connection |
| */ |
| formatConnectionName: function (connection) { |
| if (!nfCommon.isBlank(connection.name)) { |
| return connection.name; |
| } else if (nfCommon.isDefinedAndNotNull(connection.selectedRelationships)) { |
| return connection.selectedRelationships.join(', '); |
| } |
| return ''; |
| }, |
| |
| /** |
| * Reloads a connection's source and destination. |
| * |
| * @param {string} sourceComponentId The connection source id |
| * @param {string} destinationComponentId The connection destination id |
| */ |
| reloadConnectionSourceAndDestination: function (sourceComponentId, destinationComponentId) { |
| if (nfCommon.isBlank(sourceComponentId) === false) { |
| var source = d3.select('#id-' + sourceComponentId); |
| if (source.empty() === false) { |
| nfGraph.reload(source); |
| } |
| } |
| if (nfCommon.isBlank(destinationComponentId) === false) { |
| var destination = d3.select('#id-' + destinationComponentId); |
| if (destination.empty() === false) { |
| nfGraph.reload(destination); |
| } |
| } |
| }, |
| |
| /** |
| * Returns the component id of the source of this processor. If the connection is attached |
| * to a port in a [sub|remote] group, the component id will be that of the group. Otherwise |
| * it is the component itself. |
| * |
| * @param {object} connection The connection in question |
| */ |
| getConnectionSourceComponentId: function (connection) { |
| var sourceId = connection.sourceId; |
| if (connection.sourceGroupId !== nfCanvas.getGroupId()) { |
| sourceId = connection.sourceGroupId; |
| } |
| return sourceId; |
| }, |
| |
| /** |
| * Returns the component id of the source of this processor. If the connection is attached |
| * to a port in a [sub|remote] group, the component id will be that of the group. Otherwise |
| * it is the component itself. |
| * |
| * @param {object} connection The connection in question |
| */ |
| getConnectionDestinationComponentId: function (connection) { |
| var destinationId = connection.destinationId; |
| if (connection.destinationGroupId !== nfCanvas.getGroupId()) { |
| destinationId = connection.destinationGroupId; |
| } |
| return destinationId; |
| }, |
| |
| /** |
| * Attempts to restore a persisted view. Returns a flag that indicates if the |
| * view was restored. |
| */ |
| restoreUserView: function () { |
| var viewRestored = false; |
| |
| try { |
| // see if we can restore the view position from storage |
| var name = config.storage.namePrefix + nfCanvas.getGroupId(); |
| var item = nfStorage.getItem(name); |
| |
| // ensure the item is valid |
| if (nfCommon.isDefinedAndNotNull(item)) { |
| if (isFinite(item.scale) && isFinite(item.translateX) && isFinite(item.translateY)) { |
| // restore previous view |
| nfCanvas.View.transform([item.translateX, item.translateY], item.scale); |
| |
| // mark the view was restore |
| viewRestored = true; |
| } |
| } |
| } catch (e) { |
| // likely could not parse item.. ignoring |
| } |
| |
| return viewRestored; |
| }, |
| |
| /** |
| * Gets the origin of the bounding box for the specified selection. |
| * |
| * @argument {selection} selection The selection |
| */ |
| getOrigin: function (selection) { |
| var origin = {}; |
| |
| selection.each(function (d) { |
| var selected = d3.select(this); |
| if (!nfCanvasUtils.isConnection(selected)) { |
| if (nfCommon.isUndefined(origin.x) || d.position.x < origin.x) { |
| origin.x = d.position.x; |
| } |
| if (nfCommon.isUndefined(origin.y) || d.position.y < origin.y) { |
| origin.y = d.position.y; |
| } |
| } |
| }); |
| |
| return origin; |
| }, |
| |
| /** |
| * Get a BoundingClientRect, normalized to the canvas, that encompasses all nodes in a given selection. |
| * |
| * @param selection |
| * @returns {*} BoundingClientRect |
| */ |
| getSelectionBoundingClientRect: function (selection) { |
| var scale = nfCanvas.View.getScale(); |
| var translate = nfCanvas.View.getTranslate(); |
| |
| var initialBBox = { |
| x: Number.MAX_VALUE, |
| y: Number.MAX_VALUE, |
| right: Number.MIN_VALUE, |
| bottom: Number.MIN_VALUE, |
| translate: nfCanvas.View.getTranslate() |
| }; |
| |
| var bbox = selection.nodes().reduce(function (aggregateBBox, node) { |
| var rect = node.getBoundingClientRect(); |
| aggregateBBox.x = Math.min(rect.x, aggregateBBox.x); |
| aggregateBBox.y = Math.min(rect.y, aggregateBBox.y); |
| aggregateBBox.right = Math.max(rect.right, aggregateBBox.right); |
| aggregateBBox.bottom = Math.max(rect.bottom, aggregateBBox.bottom); |
| |
| return aggregateBBox; |
| }, initialBBox); |
| |
| // normalize the bounding box with scale and translate |
| bbox.x = (bbox.x - translate[0]) / scale; |
| bbox.y = (bbox.y - translate[1]) / scale; |
| bbox.right = (bbox.right - translate[0]) / scale; |
| bbox.bottom = (bbox.bottom - translate[1]) / scale; |
| |
| bbox.width = bbox.right - bbox.x; |
| bbox.height = bbox.bottom - bbox.y; |
| bbox.top = bbox.y; |
| bbox.left = bbox.x; |
| |
| return bbox; |
| }, |
| |
| /** |
| * Applies a translation to BoundingClientRect. |
| * |
| * @param boundingClientRect |
| * @param translate |
| * @returns {{top: number, left: number, bottom: number, x: number, width: number, y: number, right: number, height: number}} |
| */ |
| translateBoundingClientRect: function (boundingClientRect, translate) { |
| if (nfCommon.isUndefinedOrNull(translate)) { |
| if (nfCommon.isDefinedAndNotNull(boundingClientRect.translate)) { |
| translate = boundingClientRect.translate; |
| } else { |
| translate = nfCanvas.View.getTranslate(); |
| } |
| } |
| return { |
| x: boundingClientRect.x - translate[0], |
| y: boundingClientRect.y - translate[1], |
| left: boundingClientRect.left - translate[0], |
| right: boundingClientRect.right - translate[0], |
| top: boundingClientRect.top - translate[1], |
| bottom: boundingClientRect.bottom - translate[1], |
| width: boundingClientRect.width, |
| height: boundingClientRect.height |
| } |
| }, |
| |
| /** |
| * Moves the specified components into the current parent group. |
| * |
| * @param {selection} components |
| */ |
| moveComponentsToParent: function (components) { |
| var groupId = nfCanvas.getParentGroupId(); |
| |
| // if the group id is null, we're already in the top most group |
| if (groupId === null) { |
| nfDialog.showOkDialog({ |
| headerText: 'Process Group', |
| dialogContent: 'Components are already in the topmost group.' |
| }); |
| } else { |
| moveComponents(components, groupId); |
| } |
| }, |
| |
| /** |
| * Moves the specified components into the specified group. |
| * |
| * @param {selection} components The components to move |
| * @param {selection} group The destination group |
| */ |
| moveComponents: function (components, group) { |
| var groupData = group.datum(); |
| |
| // move the components into the destination and... |
| moveComponents(components, groupData.id).done(function () { |
| // reload the target group |
| nfCanvasUtils.getComponentByType('ProcessGroup').reload(groupData.id); |
| }); |
| }, |
| |
| /** |
| * Removes any dangling edges. All components are retained as well as any |
| * edges whose source and destination are also retained. |
| * |
| * @param {selection} selection |
| * @returns {array} |
| */ |
| trimDanglingEdges: function (selection) { |
| // returns whether the source and destination of the specified connection are present in the specified selection |
| var keepConnection = function (connection) { |
| var sourceComponentId = nfCanvasUtils.getConnectionSourceComponentId(connection); |
| var destinationComponentId = nfCanvasUtils.getConnectionDestinationComponentId(connection); |
| |
| // determine if both source and destination are selected |
| var includesSource = false; |
| var includesDestination = false; |
| selection.each(function (d) { |
| if (d.id === sourceComponentId) { |
| includesSource = true; |
| } |
| if (d.id === destinationComponentId) { |
| includesDestination = true; |
| } |
| }); |
| |
| return includesSource && includesDestination; |
| }; |
| |
| // include all components and connections whose source/destination are also selected |
| return selection.filter(function (d) { |
| if (d.type === 'Connection') { |
| return keepConnection(d); |
| } else { |
| return true; |
| } |
| }); |
| }, |
| |
| /** |
| * Determines if the component in the specified selection is a valid connection source. |
| * |
| * @param {selection} selection The selection |
| * @return {boolean} Whether the selection is a valid connection source |
| */ |
| isValidConnectionSource: function (selection) { |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| // always allow connections from process groups |
| if (nfCanvasUtils.isProcessGroup(selection)) { |
| return true; |
| } |
| |
| // require read and write for a connection source since we'll need to read the source to obtain valid relationships, etc |
| if (nfCanvasUtils.canRead(selection) === false || nfCanvasUtils.canModify(selection) === false) { |
| return false; |
| } |
| |
| return nfCanvasUtils.isProcessor(selection) || nfCanvasUtils.isRemoteProcessGroup(selection) || |
| nfCanvasUtils.isInputPort(selection) || nfCanvasUtils.isFunnel(selection); |
| }, |
| |
| /** |
| * Determines if the component in the specified selection is a valid connection destination. |
| * |
| * @param {selection} selection The selection |
| * @return {boolean} Whether the selection is a valid connection destination |
| */ |
| isValidConnectionDestination: function (selection) { |
| if (selection.size() !== 1) { |
| return false; |
| } |
| |
| if (nfCanvasUtils.isProcessGroup(selection)) { |
| return true; |
| } |
| |
| // require write for a connection destination |
| if (nfCanvasUtils.canModify(selection) === false) { |
| return false; |
| } |
| |
| if (nfCanvasUtils.isRemoteProcessGroup(selection) || nfCanvasUtils.isOutputPort(selection) || nfCanvasUtils.isFunnel(selection)) { |
| return true; |
| } |
| |
| // if processor, ensure it supports input |
| if (nfCanvasUtils.isProcessor(selection)) { |
| var destinationData = selection.datum(); |
| return destinationData.inputRequirement !== 'INPUT_FORBIDDEN'; |
| } |
| }, |
| |
| /** |
| * Returns whether the authorizer is managed. |
| */ |
| isManagedAuthorizer: function () { |
| return nfCanvas.isManagedAuthorizer(); |
| }, |
| |
| /** |
| * Returns whether the authorizer is configurable. |
| */ |
| isConfigurableAuthorizer: function () { |
| return nfCanvas.isConfigurableAuthorizer(); |
| }, |
| |
| /** |
| * Returns whether the authorizer support configurable users and groups. |
| */ |
| isConfigurableUsersAndGroups: function () { |
| return nfCanvas.isConfigurableUsersAndGroups(); |
| }, |
| |
| /** |
| * Adds the restricted usage and the required permissions. |
| * |
| * @param additionalRestrictedUsages |
| * @param additionalRequiredPermissions |
| */ |
| addComponentRestrictions: function (additionalRestrictedUsages, additionalRequiredPermissions) { |
| additionalRestrictedUsages.each(function (componentRestrictions, requiredPermissionId) { |
| if (!restrictedUsage.has(requiredPermissionId)) { |
| restrictedUsage.set(requiredPermissionId, []); |
| } |
| |
| componentRestrictions.forEach(function (componentRestriction) { |
| restrictedUsage.get(requiredPermissionId).push(componentRestriction); |
| }); |
| }); |
| additionalRequiredPermissions.each(function (requiredPermissionLabel, requiredPermissionId) { |
| if (!requiredPermissions.has(requiredPermissionId)) { |
| requiredPermissions.set(requiredPermissionId, requiredPermissionLabel); |
| } |
| }); |
| }, |
| |
| /** |
| * Gets the component restrictions and the require permissions. |
| * |
| * @returns {{restrictedUsage: map, requiredPermissions: map}} component restrictions |
| */ |
| getComponentRestrictions: function () { |
| return { |
| restrictedUsage: restrictedUsage, |
| requiredPermissions: requiredPermissions |
| }; |
| }, |
| |
| /** |
| * Set the group id. |
| * |
| * @argument {string} gi The group id |
| */ |
| setGroupId: function (gi) { |
| return nfCanvas.setGroupId(gi); |
| }, |
| |
| /** |
| * Get the group id. |
| */ |
| getGroupId: function () { |
| return nfCanvas.getGroupId(); |
| }, |
| |
| /** |
| * Set the parameter context. |
| * |
| * @argument {string} pc The parameter context |
| */ |
| setParameterContext: function (pc) { |
| return nfCanvas.setParameterContext(pc); |
| }, |
| |
| /** |
| * Get the parameter context. |
| */ |
| getParameterContext: function () { |
| return nfCanvas.getParameterContext(); |
| }, |
| |
| /** |
| * Get the group name. |
| */ |
| getGroupName: function () { |
| return nfCanvas.getGroupName(); |
| }, |
| |
| /** |
| * Get the parent group id. |
| */ |
| getParentGroupId: function () { |
| return nfCanvas.getParentGroupId(); |
| }, |
| |
| /** |
| * Reloads the status for the entire canvas (components and flow.) |
| * |
| * @param {string} groupId Optional, specific group id to reload the canvas to |
| */ |
| reload: function (groupId) { |
| return nfCanvas.reload({ |
| 'transition': true |
| }, groupId); |
| }, |
| |
| /** |
| * Whether the current user can read from this group. |
| * |
| * @returns {boolean} can write |
| */ |
| canReadCurrentGroup: function () { |
| return nfCanvas.canRead(); |
| }, |
| |
| /** |
| * Whether the current user can write in this group. |
| * |
| * @returns {boolean} can write |
| */ |
| canWriteCurrentGroup: function () { |
| return nfCanvas.canWrite(); |
| }, |
| |
| /** |
| * Gets the current scale. |
| */ |
| getCanvasScale: function () { |
| return nfCanvas.View.getScale(); |
| }, |
| |
| /** |
| * Gets the current translation. |
| */ |
| getCanvasTranslate: function () { |
| return nfCanvas.View.getTranslate(); |
| }, |
| |
| /** |
| * Translate the canvas by the specified [x, y] |
| * |
| * @param {array} translate [x, y] to translate by |
| */ |
| translateCanvas: function (translate) { |
| nfCanvas.View.translate(translate); |
| }, |
| |
| /** |
| * Zooms to fit the entire graph on the canvas. |
| */ |
| fitCanvas: function () { |
| return nfCanvas.View.fit(); |
| }, |
| |
| /** |
| * Zooms in a single zoom increment. |
| */ |
| zoomInCanvas: function () { |
| return nfCanvas.View.zoomIn(); |
| }, |
| |
| /** |
| * Zooms out a single zoom increment. |
| */ |
| zoomOutCanvas: function () { |
| return nfCanvas.View.zoomOut(); |
| }, |
| |
| /** |
| * Zooms to the actual size (1 to 1). |
| */ |
| actualSizeCanvas: function () { |
| return nfCanvas.View.actualSize(); |
| }, |
| |
| /** |
| * Whether or not a component should be rendered based solely on the current scale. |
| * |
| * @returns {Boolean} |
| */ |
| shouldRenderPerScale: function () { |
| return nfCanvas.View.shouldRenderPerScale(); |
| }, |
| |
| /** |
| * Gets the canvas offset. |
| */ |
| getCanvasOffset: function () { |
| return nfCanvas.CANVAS_OFFSET; |
| } |
| }; |
| return nfCanvasUtils; |
| })); |