[NIFI-96] add align horizontal and align vertical capability to components on the canvas. This closes #1354

This commit is contained in:
Scott Aslan 2016-12-22 16:06:38 -05:00 committed by Matt Gilman
parent e65aad8fe6
commit 5ea17d30c5
5 changed files with 353 additions and 140 deletions

View File

@ -97,6 +97,11 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
margin: -2px;
}
/*shift rotated font awesome icons*/
.fa-rotate-90 {
left: -2px !important;
}
body {
display: block;
font-family: Roboto, sans-serif;

View File

@ -75,6 +75,13 @@ nf.Actions = (function () {
});
};
// determine if the source of this connection is part of the selection
var isSourceSelected = function (connection, selection) {
return selection.filter(function (d) {
return nf.CanvasUtils.getConnectionSourceComponentId(connection) === d.id;
}).size() > 0;
};
return {
/**
* Initializes the actions.
@ -867,7 +874,7 @@ nf.Actions = (function () {
// refresh the birdseye
nf.Birdseye.refresh();
// inform Angular app values have changed
nf.ng.Bridge.digest();
}).fail(nf.Common.handleAjaxError);
@ -1094,6 +1101,145 @@ nf.Actions = (function () {
nf.ComponentState.showState(processor, nf.CanvasUtils.isConfigurable(selection));
},
/**
* Aligns the components in the specified selection vertically along the center of the components.
*
* @param {array} selection The selection
*/
alignVertical: function (selection) {
var updates = d3.map();
// ensure every component is writable
if (nf.CanvasUtils.canModify(selection) === false) {
nf.Dialog.showOkDialog({
headerText: 'Component Position',
dialogContent: 'Must be authorized to modify every component selected.'
});
return;
}
// determine the extent
var minX = null, maxX = null;
selection.each(function (d) {
if (d.type !== "Connection") {
if (minX === null || d.position.x < minX) {
minX = d.position.x;
}
var componentMaxX = d.position.x + d.dimensions.width;
if (maxX === null || componentMaxX > maxX) {
maxX = componentMaxX;
}
}
});
var center = (minX + maxX) / 2;
// align all components left
selection.each(function(d) {
if (d.type !== "Connection") {
var delta = {
x: center - (d.position.x + d.dimensions.width / 2),
y: 0
};
// if this component is already centered, no need to updated it
if (delta.x !== 0) {
// consider any connections
var connections = nf.Connection.getComponentConnections(d.id);
$.each(connections, function(_, connection) {
var connectionSelection = d3.select('#id-' + connection.id);
if (!updates.has(connection.id) && nf.CanvasUtils.getConnectionSourceComponentId(connection) === nf.CanvasUtils.getConnectionDestinationComponentId(connection)) {
// this connection is self looping and hasn't been updated by the delta yet
var connectionUpdate = nf.Draggable.updateConnectionPosition(nf.Connection.get(connection.id), delta);
if (connectionUpdate !== null) {
updates.set(connection.id, connectionUpdate);
}
} else if (!updates.has(connection.id) && connectionSelection.classed('selected') && nf.CanvasUtils.canModify(connectionSelection)) {
// this is a selected connection that hasn't been updated by the delta yet
if (nf.CanvasUtils.getConnectionSourceComponentId(connection) === d.id || !isSourceSelected(connection, selection)) {
// the connection is either outgoing or incoming when the source of the connection is not part of the selection
var connectionUpdate = nf.Draggable.updateConnectionPosition(nf.Connection.get(connection.id), delta);
if (connectionUpdate !== null) {
updates.set(connection.id, connectionUpdate);
}
}
}
});
updates.set(d.id, nf.Draggable.updateComponentPosition(d, delta));
}
}
});
nf.Draggable.refreshConnections(updates);
},
/**
* Aligns the components in the specified selection horizontally along the center of the components.
*
* @param {array} selection The selection
*/
alignHorizontal: function (selection) {
var updates = d3.map();
// ensure every component is writable
if (nf.CanvasUtils.canModify(selection) === false) {
nf.Dialog.showOkDialog({
headerText: 'Component Position',
dialogContent: 'Must be authorized to modify every component selected.'
});
return;
}
// determine the extent
var minY = null, maxY = null;
selection.each(function (d) {
if (d.type !== "Connection") {
if (minY === null || d.position.y < minY) {
minY = d.position.y;
}
var componentMaxY = d.position.y + d.dimensions.height;
if (maxY === null || componentMaxY > maxY) {
maxY = componentMaxY;
}
}
});
var center = (minY + maxY) / 2;
// align all components with top most component
selection.each(function(d) {
if (d.type !== "Connection") {
var delta = {
x: 0,
y: center - (d.position.y + d.dimensions.height / 2)
};
// if this component is already centered, no need to updated it
if (delta.y !== 0) {
// consider any connections
var connections = nf.Connection.getComponentConnections(d.id);
$.each(connections, function(_, connection) {
var connectionSelection = d3.select('#id-' + connection.id);
if (!updates.has(connection.id) && nf.CanvasUtils.getConnectionSourceComponentId(connection) === nf.CanvasUtils.getConnectionDestinationComponentId(connection)) {
// this connection is self looping and hasn't been updated by the delta yet
var connectionUpdate = nf.Draggable.updateConnectionPosition(nf.Connection.get(connection.id), delta);
if (connectionUpdate !== null) {
updates.set(connection.id, connectionUpdate);
}
} else if (!updates.has(connection.id) && connectionSelection.classed('selected') && nf.CanvasUtils.canModify(connectionSelection)) {
// this is a selected connection that hasn't been updated by the delta yet
if (nf.CanvasUtils.getConnectionSourceComponentId(connection) === d.id || !isSourceSelected(connection, selection)) {
// the connection is either outgoing or incoming when the source of the connection is not part of the selection
var connectionUpdate = nf.Draggable.updateConnectionPosition(nf.Connection.get(connection.id), delta);
if (connectionUpdate !== null) {
updates.set(connection.id, connectionUpdate);
}
}
}
});
updates.set(d.id, nf.Draggable.updateComponentPosition(d, delta));
}
}
});
nf.Draggable.refreshConnections(updates);
},
/**
* Opens the fill color dialog for the component in the specified selection.
*

View File

@ -545,6 +545,34 @@ nf.CanvasUtils = (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 nf.CanvasUtils.isConnection(connection);
});
// require multiple selections besides connections
if (selection.size() - selectedConnections.size() < 2) {
canAlign = false;
}
// require write permissions
if (nf.CanvasUtils.canModify(selection) === false) {
canAlign = false;
}
return canAlign;
},
/**
* Determines if the specified selection is colorable (in a single action).

View File

@ -157,9 +157,18 @@ nf.ContextMenu = (function () {
return nf.CanvasUtils.isConnection(selection);
};
/**
* Determines whether the components in the specified selection are alignable.
*
* @param {selection} selection The selection
*/
var canAlign = function (selection) {
return nf.CanvasUtils.canAlign(selection);
};
/**
* Determines whether the components in the specified selection are colorable.
*
*
* @param {selection} selection The selection
*/
var isColorable = function (selection) {
@ -435,7 +444,9 @@ nf.ContextMenu = (function () {
{condition: canMoveToParent, menuItem: {clazz: 'fa fa-arrows', text: 'Move to parent group', action: 'moveIntoParent'}},
{condition: canListQueue, menuItem: {clazz: 'fa fa-list', text: 'List queue', action: 'listQueue'}},
{condition: canEmptyQueue, menuItem: {clazz: 'fa fa-minus-circle', text: 'Empty queue', action: 'emptyQueue'}},
{condition: isDeletable, menuItem: {clazz: 'fa fa-trash', text: 'Delete', action: 'delete'}}
{condition: isDeletable, menuItem: {clazz: 'fa fa-trash', text: 'Delete', action: 'delete'}},
{condition: canAlign, menuItem: {clazz: 'fa fa-align-center', text: 'Align vertical', action: 'alignVertical'}},
{condition: canAlign, menuItem: {clazz: 'fa fa-align-center fa-rotate-90', text: 'Align horizontal', action: 'alignHorizontal'}}
];
return {

View File

@ -23,7 +23,7 @@ nf.Draggable = (function () {
/**
* Updates the positioning of all selected components.
*
*
* @param {selection} dragSelection The current drag selection
*/
var updateComponentsPosition = function (dragSelection) {
@ -40,107 +40,6 @@ nf.Draggable = (function () {
if (delta.x === 0 && delta.y === 0) {
return;
}
var updateComponentPosition = function(d) {
var newPosition = {
'x': d.position.x + delta.x,
'y': d.position.y + delta.y
};
// build the entity
var entity = {
'revision': nf.Client.getRevision(d),
'component': {
'id': d.id,
'position': newPosition
}
};
// update the component positioning
return $.Deferred(function (deferred) {
$.ajax({
type: 'PUT',
url: d.uri,
data: JSON.stringify(entity),
dataType: 'json',
contentType: 'application/json'
}).done(function (response) {
// update the component
nf[d.type].set(response);
// resolve with an object so we can refresh when finished
deferred.resolve({
type: d.type,
id: d.id
});
}).fail(function (xhr, status, error) {
if (xhr.status === 400 || xhr.status === 404 || xhr.status === 409) {
nf.Dialog.showOkDialog({
headerText: 'Component Position',
dialogContent: nf.Common.escapeHtml(xhr.responseText)
});
} else {
nf.Common.handleAjaxError(xhr, status, error);
}
deferred.reject();
});
}).promise();
};
var updateConnectionPosition = function(d) {
// only update if necessary
if (d.bends.length === 0) {
return null;
}
// calculate the new bend points
var newBends = $.map(d.bends, function (bend) {
return {
x: bend.x + delta.x,
y: bend.y + delta.y
};
});
var entity = {
'revision': nf.Client.getRevision(d),
'component': {
id: d.id,
bends: newBends
}
};
// update the component positioning
return $.Deferred(function (deferred) {
$.ajax({
type: 'PUT',
url: d.uri,
data: JSON.stringify(entity),
dataType: 'json',
contentType: 'application/json'
}).done(function (response) {
// update the component
nf.Connection.set(response);
// resolve with an object so we can refresh when finished
deferred.resolve({
type: d.type,
id: d.id
});
}).fail(function (xhr, status, error) {
if (xhr.status === 400 || xhr.status === 404 || xhr.status === 409) {
nf.Dialog.showOkDialog({
headerText: 'Component Position',
dialogContent: nf.Common.escapeHtml(xhr.responseText)
});
} else {
nf.Common.handleAjaxError(xhr, status, error);
}
deferred.reject();
});
}).promise();
};
var selectedConnections = d3.selectAll('g.connection.selected');
var selectedComponents = d3.selectAll('g.component.selected');
@ -156,55 +55,30 @@ nf.Draggable = (function () {
// go through each selected connection
selectedConnections.each(function (d) {
var connectionUpdate = updateConnectionPosition(d);
var connectionUpdate = nf.Draggable.updateConnectionPosition(d, delta);
if (connectionUpdate !== null) {
updates.set(d.id, connectionUpdate);
}
});
// go through each selected component
selectedComponents.each(function (d) {
// consider any self looping connections
var connections = nf.Connection.getComponentConnections(d.id);
$.each(connections, function(_, connection) {
if (!updates.has(connection.id) && nf.CanvasUtils.getConnectionSourceComponentId(connection) === nf.CanvasUtils.getConnectionDestinationComponentId(connection)) {
var connectionUpdate = updateConnectionPosition(nf.Connection.get(connection.id));
var connectionUpdate = nf.Draggable.updateConnectionPosition(nf.Connection.get(connection.id), delta);
if (connectionUpdate !== null) {
updates.set(connection.id, connectionUpdate);
}
}
});
// consider the component itself
updates.set(d.id, updateComponentPosition(d));
updates.set(d.id, nf.Draggable.updateComponentPosition(d, delta));
});
// wait for all updates to complete
$.when.apply(window, updates.values()).done(function () {
var dragged = $.makeArray(arguments);
var connections = d3.set();
// refresh this component
$.each(dragged, function (_, component) {
// check if the component in question is a connection
if (component.type === 'Connection') {
connections.add(component.id);
} else {
// get connections that need to be refreshed because its attached to this component
var componentConnections = nf.Connection.getComponentConnections(component.id);
$.each(componentConnections, function (_, connection) {
connections.add(connection.id);
});
}
});
// refresh the connections
connections.forEach(function (connectionId) {
nf.Connection.refresh(connectionId);
});
}).always(function(){
nf.Birdseye.refresh();
});
nf.Draggable.refreshConnections(updates);
};
/**
@ -246,7 +120,7 @@ nf.Draggable = (function () {
// lazily create the drag selection box
if (dragSelection.empty()) {
// get the current selection
// get the current selection
var selection = d3.selectAll('g.component.selected');
// determine the appropriate bounding box
@ -327,10 +201,159 @@ nf.Draggable = (function () {
dragSelection.remove();
});
},
/**
* Update the component's position
*
* @param d The component
* @param delta The change in position
* @returns {*}
*/
updateComponentPosition: function(d, delta) {
var newPosition = {
'x': d.position.x + delta.x,
'y': d.position.y + delta.y
};
// build the entity
var entity = {
'revision': nf.Client.getRevision(d),
'component': {
'id': d.id,
'position': newPosition
}
};
// update the component positioning
return $.Deferred(function (deferred) {
$.ajax({
type: 'PUT',
url: d.uri,
data: JSON.stringify(entity),
dataType: 'json',
contentType: 'application/json'
}).done(function (response) {
// update the component
nf[d.type].set(response);
// resolve with an object so we can refresh when finished
deferred.resolve({
type: d.type,
id: d.id
});
}).fail(function (xhr, status, error) {
if (xhr.status === 400 || xhr.status === 404 || xhr.status === 409) {
nf.Dialog.showOkDialog({
headerText: 'Component Position',
dialogContent: nf.Common.escapeHtml(xhr.responseText)
});
} else {
nf.Common.handleAjaxError(xhr, status, error);
}
deferred.reject();
});
}).promise();
},
/**
* Update the connection's position
*
* @param d The connection
* @param delta The change in position
* @returns {*}
*/
updateConnectionPosition: function(d, delta) {
// only update if necessary
if (d.bends.length === 0) {
return null;
}
// calculate the new bend points
var newBends = $.map(d.bends, function (bend) {
return {
x: bend.x + delta.x,
y: bend.y + delta.y
};
});
var entity = {
'revision': nf.Client.getRevision(d),
'component': {
id: d.id,
bends: newBends
}
};
// update the component positioning
return $.Deferred(function (deferred) {
$.ajax({
type: 'PUT',
url: d.uri,
data: JSON.stringify(entity),
dataType: 'json',
contentType: 'application/json'
}).done(function (response) {
// update the component
nf.Connection.set(response);
// resolve with an object so we can refresh when finished
deferred.resolve({
type: d.type,
id: d.id
});
}).fail(function (xhr, status, error) {
if (xhr.status === 400 || xhr.status === 404 || xhr.status === 409) {
nf.Dialog.showOkDialog({
headerText: 'Component Position',
dialogContent: nf.Common.escapeHtml(xhr.responseText)
});
} else {
nf.Common.handleAjaxError(xhr, status, error);
}
deferred.reject();
});
}).promise();
},
/**
* Refresh the connections after dragging a component
*
* @param updates
*/
refreshConnections: function(updates) {
// wait for all updates to complete
$.when.apply(window, updates.values()).done(function () {
var dragged = $.makeArray(arguments);
var connections = d3.set();
// refresh this component
$.each(dragged, function (_, component) {
// check if the component in question is a connection
if (component.type === 'Connection') {
connections.add(component.id);
} else {
// get connections that need to be refreshed because its attached to this component
var componentConnections = nf.Connection.getComponentConnections(component.id);
$.each(componentConnections, function (_, connection) {
connections.add(connection.id);
});
}
});
// refresh the connections
connections.forEach(function (connectionId) {
nf.Connection.refresh(connectionId);
});
}).always(function(){
nf.Birdseye.refresh();
});
},
/**
* Activates the drag behavior for the components in the specified selection.
*
*
* @param {selection} components
*/
activate: function (components) {
@ -339,7 +362,7 @@ nf.Draggable = (function () {
/**
* Deactivates the drag behavior for the components in the specified selection.
*
*
* @param {selection} components
*/
deactivate: function (components) {