blob: 5588422db516b2ed637ee0ccfb356514a61cc4a8 [file] [log] [blame]
/*
* 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;
}));