mirror of https://github.com/apache/nifi.git
NIFI-4839 - Implemented auto-layout when importing the PG. Will find an available spot on a canvas which doesn't overlap with other components and is as close to the canvas center as possible.
This commit is contained in:
parent
9c3594ded6
commit
e3cc7bee05
|
@ -84,5 +84,9 @@
|
|||
<artifactId>commons-io</artifactId>
|
||||
<version>2.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jayway.jsonpath</groupId>
|
||||
<artifactId>json-path</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package org.apache.nifi.toolkit.cli.impl.client.nifi;
|
||||
|
||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.impl.PgBox;
|
||||
import org.apache.nifi.web.api.entity.CurrentUserEntity;
|
||||
import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
|
||||
import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
|
||||
|
@ -48,6 +49,16 @@ public interface FlowClient {
|
|||
*/
|
||||
ProcessGroupFlowEntity getProcessGroup(String id) throws NiFiClientException, IOException;
|
||||
|
||||
/**
|
||||
* Suggest a location for the new process group on a canvas, within a given process group.
|
||||
* Will locate to the right and then bottom and offset for the size of the PG box.
|
||||
* @param parentId
|
||||
* @return
|
||||
* @throws NiFiClientException
|
||||
* @throws IOException
|
||||
*/
|
||||
PgBox getSuggestedProcessGroupCoordinates(String parentId) throws NiFiClientException, IOException;
|
||||
|
||||
/**
|
||||
* Schedules the components of a process group.
|
||||
*
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
*/
|
||||
package org.apache.nifi.toolkit.cli.impl.client.nifi.impl;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import net.minidev.json.JSONArray;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.FlowClient;
|
||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
|
||||
|
@ -27,9 +29,13 @@ import org.apache.nifi.web.api.entity.VersionedFlowSnapshotMetadataSetEntity;
|
|||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.WebTarget;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Jersey implementation of FlowClient.
|
||||
|
@ -78,6 +84,41 @@ public class JerseyFlowClient extends AbstractJerseyClient implements FlowClient
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public PgBox getSuggestedProcessGroupCoordinates(String parentId) throws NiFiClientException, IOException {
|
||||
if (StringUtils.isBlank(parentId)) {
|
||||
throw new IllegalArgumentException("Process group id cannot be null");
|
||||
}
|
||||
|
||||
return executeAction("Error retrieving process group flow", () -> {
|
||||
final WebTarget target = flowTarget
|
||||
.path("process-groups/{id}")
|
||||
.resolveTemplate("id", parentId);
|
||||
|
||||
Response response = getRequestBuilder(target).get();
|
||||
|
||||
String json = response.readEntity(String.class);
|
||||
|
||||
JSONArray jsonArray = JsonPath.compile("$..position").read(json);
|
||||
|
||||
if (jsonArray.isEmpty()) {
|
||||
// it's an empty nifi canvas, nice to align
|
||||
return PgBox.CANVAS_CENTER;
|
||||
}
|
||||
|
||||
List<PgBox> coords = new HashSet<>(jsonArray) // de-dup the initial set
|
||||
.stream().map(Map.class::cast)
|
||||
.map(m -> new PgBox(Double.valueOf(m.get("x").toString()).intValue(),
|
||||
Double.valueOf(m.get("y").toString()).intValue()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PgBox freeSpot = coords.get(0).findFreeSpace(coords);
|
||||
|
||||
return freeSpot;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduleComponentsEntity scheduleProcessGroupComponents(
|
||||
final String processGroupId, final ScheduleComponentsEntity scheduleComponentsEntity)
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
package org.apache.nifi.toolkit.cli.impl.client.nifi.impl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Represents the bounding box the Processing Group and some placement logic.
|
||||
*/
|
||||
public class PgBox implements Comparable<PgBox> {
|
||||
// values as specified in the nf-process-group.js file
|
||||
public static final int PG_SIZE_WIDTH = 380;
|
||||
public static final int PG_SIZE_HEIGHT = 172;
|
||||
// minimum whitespace between PG elements for auto-layout
|
||||
public static final int PG_SPACING = 50;
|
||||
public int x;
|
||||
public int y;
|
||||
|
||||
public static final PgBox CANVAS_CENTER = new PgBox(0, 0);
|
||||
|
||||
public PgBox(int x, int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return distance from a (0, 0) point.
|
||||
*/
|
||||
public int distance() {
|
||||
// a simplified distance formula because the other coord is (0, 0)
|
||||
return (int) Math.hypot(x, y);
|
||||
}
|
||||
|
||||
|
||||
public boolean intersects(PgBox other) {
|
||||
// adapted for java.awt Rectangle, we don't want to import it
|
||||
// assume everything to be of the PG size for simplicity
|
||||
int tw = PG_SIZE_WIDTH;
|
||||
int th = PG_SIZE_HEIGHT;
|
||||
// 2nd pg box includes spacers
|
||||
int rw = PG_SIZE_WIDTH;
|
||||
int rh = PG_SIZE_HEIGHT;
|
||||
if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) {
|
||||
return false;
|
||||
}
|
||||
double tx = this.x;
|
||||
double ty = this.y;
|
||||
double rx = other.x;
|
||||
double ry = other.y;
|
||||
rw += rx;
|
||||
rh += ry;
|
||||
tw += tx;
|
||||
th += ty;
|
||||
// overflow || intersect
|
||||
return ((rw < rx || rw > tx) &&
|
||||
(rh < ry || rh > ty) &&
|
||||
(tw < tx || tw > rx) &&
|
||||
(th < ty || th > ry));
|
||||
}
|
||||
|
||||
|
||||
public PgBox findFreeSpace(List<PgBox> allCoords) {
|
||||
// sort by distance to (0.0)
|
||||
List<PgBox> byClosest = allCoords.stream().sorted().collect(Collectors.toList());
|
||||
|
||||
// search to the right
|
||||
List<PgBox> freeSpots = byClosest.stream().filter(other ->
|
||||
byClosest.stream().noneMatch(other.right()::intersects)
|
||||
).map(PgBox::right).collect(Collectors.toList()); // save a 'transformed' spot 'to the right'
|
||||
|
||||
// search down
|
||||
freeSpots.addAll(byClosest.stream().filter(other ->
|
||||
byClosest.stream().noneMatch(other.down()::intersects)
|
||||
).map(PgBox::down).collect(Collectors.toList()));
|
||||
|
||||
// search left
|
||||
freeSpots.addAll(byClosest.stream().filter(other ->
|
||||
byClosest.stream().noneMatch(other.left()::intersects)
|
||||
).map(PgBox::left).collect(Collectors.toList()));
|
||||
|
||||
// search above
|
||||
freeSpots.addAll(byClosest.stream().filter(other ->
|
||||
byClosest.stream().noneMatch(other.up()::intersects)
|
||||
).map(PgBox::up).collect(Collectors.toList()));
|
||||
|
||||
// return a free spot closest to (0, 0)
|
||||
return freeSpots.stream().sorted().findFirst().orElse(CANVAS_CENTER);
|
||||
}
|
||||
|
||||
public PgBox right() {
|
||||
return new PgBox(this.x + PG_SIZE_WIDTH + PG_SPACING, this.y);
|
||||
}
|
||||
|
||||
public PgBox down() {
|
||||
return new PgBox(this.x, this.y + PG_SIZE_HEIGHT + PG_SPACING);
|
||||
}
|
||||
|
||||
public PgBox up() {
|
||||
return new PgBox(this.x, this.y - PG_SPACING - PG_SIZE_HEIGHT);
|
||||
}
|
||||
|
||||
public PgBox left() {
|
||||
return new PgBox(this.x - PG_SPACING - PG_SIZE_WIDTH, this.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(PgBox other) {
|
||||
return this.distance() - other.distance();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PgBox pgBox = (PgBox) o;
|
||||
return Double.compare(pgBox.x, x) == 0 &&
|
||||
Double.compare(pgBox.y, y) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(x, y);
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import org.apache.nifi.toolkit.cli.impl.client.nifi.FlowClient;
|
|||
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClient;
|
||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
|
||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.ProcessGroupClient;
|
||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.impl.PgBox;
|
||||
import org.apache.nifi.toolkit.cli.impl.command.CommandOption;
|
||||
import org.apache.nifi.toolkit.cli.impl.command.nifi.AbstractNiFiCommand;
|
||||
import org.apache.nifi.web.api.dto.PositionDTO;
|
||||
|
@ -62,14 +63,15 @@ public class PGImport extends AbstractNiFiCommand {
|
|||
final String flowId = getRequiredArg(properties, CommandOption.FLOW_ID);
|
||||
final Integer flowVersion = getRequiredIntArg(properties, CommandOption.FLOW_VERSION);
|
||||
|
||||
// TODO - do we actually want the client to deal with X/Y coordinates? drop the args from API?
|
||||
Integer posX = getIntArg(properties, CommandOption.POS_X);
|
||||
if (posX == null) {
|
||||
posX = new Integer(0);
|
||||
posX = 0;
|
||||
}
|
||||
|
||||
Integer posY = getIntArg(properties, CommandOption.POS_Y);
|
||||
if (posY == null) {
|
||||
posY = new Integer(0);
|
||||
posY = 0;
|
||||
}
|
||||
|
||||
// get the optional id of the parent PG, otherwise fallback to the root group
|
||||
|
@ -85,12 +87,15 @@ public class PGImport extends AbstractNiFiCommand {
|
|||
versionControlInfo.setFlowId(flowId);
|
||||
versionControlInfo.setVersion(flowVersion);
|
||||
|
||||
PgBox pgBox = client.getFlowClient().getSuggestedProcessGroupCoordinates(parentPgId);
|
||||
|
||||
final PositionDTO posDto = new PositionDTO();
|
||||
posDto.setX(posX.doubleValue());
|
||||
posDto.setY(posY.doubleValue());
|
||||
posDto.setX(Integer.valueOf(pgBox.x).doubleValue());
|
||||
posDto.setY(Integer.valueOf(pgBox.y).doubleValue());
|
||||
|
||||
final ProcessGroupDTO pgDto = new ProcessGroupDTO();
|
||||
pgDto.setVersionControlInformation(versionControlInfo);
|
||||
pgDto.setPosition(posDto);
|
||||
|
||||
final ProcessGroupEntity pgEntity = new ProcessGroupEntity();
|
||||
pgEntity.setComponent(pgDto);
|
||||
|
|
Loading…
Reference in New Issue