From 5ea17d30c5858fd9e56498e01a6cbd9fa3423c73 Mon Sep 17 00:00:00 2001 From: Scott Aslan Date: Thu, 22 Dec 2016 16:06:38 -0500 Subject: [PATCH] [NIFI-96] add align horizontal and align vertical capability to components on the canvas. This closes #1354 --- .../src/main/webapp/css/common-ui.css | 5 + .../main/webapp/js/nf/canvas/nf-actions.js | 148 ++++++++- .../webapp/js/nf/canvas/nf-canvas-utils.js | 28 ++ .../webapp/js/nf/canvas/nf-context-menu.js | 15 +- .../main/webapp/js/nf/canvas/nf-draggable.js | 297 ++++++++++-------- 5 files changed, 353 insertions(+), 140 deletions(-) diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css index a0f6af7524..e871537314 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/common-ui.css @@ -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; diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js index a9c05b8f90..b27e291bef 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-actions.js @@ -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. * diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js index 1fb262eefa..e743d3dfe5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-utils.js @@ -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). diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js index 8100678164..b51ddb069a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-context-menu.js @@ -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 { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-draggable.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-draggable.js index f6ae344dae..08b45900aa 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-draggable.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-draggable.js @@ -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) {