[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; margin: -2px;
} }
/*shift rotated font awesome icons*/
.fa-rotate-90 {
left: -2px !important;
}
body { body {
display: block; display: block;
font-family: Roboto, sans-serif; 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 { return {
/** /**
* Initializes the actions. * Initializes the actions.
@ -1094,6 +1101,145 @@ nf.Actions = (function () {
nf.ComponentState.showState(processor, nf.CanvasUtils.isConfigurable(selection)); 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. * Opens the fill color dialog for the component in the specified selection.
* *

View File

@ -546,6 +546,34 @@ nf.CanvasUtils = (function () {
}); });
}, },
/**
* 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). * Determines if the specified selection is colorable (in a single action).
* *

View File

@ -157,6 +157,15 @@ nf.ContextMenu = (function () {
return nf.CanvasUtils.isConnection(selection); 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. * Determines whether the components in the specified selection are colorable.
* *
@ -435,7 +444,9 @@ nf.ContextMenu = (function () {
{condition: canMoveToParent, menuItem: {clazz: 'fa fa-arrows', text: 'Move to parent group', action: 'moveIntoParent'}}, {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: 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: 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 { return {

View File

@ -41,107 +41,6 @@ nf.Draggable = (function () {
return; 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 selectedConnections = d3.selectAll('g.connection.selected');
var selectedComponents = d3.selectAll('g.component.selected'); var selectedComponents = d3.selectAll('g.component.selected');
@ -156,7 +55,7 @@ nf.Draggable = (function () {
// go through each selected connection // go through each selected connection
selectedConnections.each(function (d) { selectedConnections.each(function (d) {
var connectionUpdate = updateConnectionPosition(d); var connectionUpdate = nf.Draggable.updateConnectionPosition(d, delta);
if (connectionUpdate !== null) { if (connectionUpdate !== null) {
updates.set(d.id, connectionUpdate); updates.set(d.id, connectionUpdate);
} }
@ -168,7 +67,7 @@ nf.Draggable = (function () {
var connections = nf.Connection.getComponentConnections(d.id); var connections = nf.Connection.getComponentConnections(d.id);
$.each(connections, function(_, connection) { $.each(connections, function(_, connection) {
if (!updates.has(connection.id) && nf.CanvasUtils.getConnectionSourceComponentId(connection) === nf.CanvasUtils.getConnectionDestinationComponentId(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) { if (connectionUpdate !== null) {
updates.set(connection.id, connectionUpdate); updates.set(connection.id, connectionUpdate);
} }
@ -176,35 +75,10 @@ nf.Draggable = (function () {
}); });
// consider the component itself // 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 nf.Draggable.refreshConnections(updates);
$.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();
});
}; };
/** /**
@ -328,6 +202,155 @@ nf.Draggable = (function () {
}); });
}, },
/**
* 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. * Activates the drag behavior for the components in the specified selection.
* *