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>
|
<artifactId>commons-io</artifactId>
|
||||||
<version>2.6</version>
|
<version>2.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.jayway.jsonpath</groupId>
|
||||||
|
<artifactId>json-path</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.nifi.toolkit.cli.impl.client.nifi;
|
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.CurrentUserEntity;
|
||||||
import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
|
import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
|
||||||
import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
|
import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
|
||||||
|
@ -48,6 +49,16 @@ public interface FlowClient {
|
||||||
*/
|
*/
|
||||||
ProcessGroupFlowEntity getProcessGroup(String id) throws NiFiClientException, IOException;
|
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.
|
* Schedules the components of a process group.
|
||||||
*
|
*
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.nifi.toolkit.cli.impl.client.nifi.impl;
|
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.commons.lang3.StringUtils;
|
||||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.FlowClient;
|
import org.apache.nifi.toolkit.cli.impl.client.nifi.FlowClient;
|
||||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
|
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.Entity;
|
||||||
import javax.ws.rs.client.WebTarget;
|
import javax.ws.rs.client.WebTarget;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Jersey implementation of FlowClient.
|
* 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
|
@Override
|
||||||
public ScheduleComponentsEntity scheduleProcessGroupComponents(
|
public ScheduleComponentsEntity scheduleProcessGroupComponents(
|
||||||
final String processGroupId, final ScheduleComponentsEntity scheduleComponentsEntity)
|
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.NiFiClient;
|
||||||
import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
|
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.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.CommandOption;
|
||||||
import org.apache.nifi.toolkit.cli.impl.command.nifi.AbstractNiFiCommand;
|
import org.apache.nifi.toolkit.cli.impl.command.nifi.AbstractNiFiCommand;
|
||||||
import org.apache.nifi.web.api.dto.PositionDTO;
|
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 String flowId = getRequiredArg(properties, CommandOption.FLOW_ID);
|
||||||
final Integer flowVersion = getRequiredIntArg(properties, CommandOption.FLOW_VERSION);
|
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);
|
Integer posX = getIntArg(properties, CommandOption.POS_X);
|
||||||
if (posX == null) {
|
if (posX == null) {
|
||||||
posX = new Integer(0);
|
posX = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Integer posY = getIntArg(properties, CommandOption.POS_Y);
|
Integer posY = getIntArg(properties, CommandOption.POS_Y);
|
||||||
if (posY == null) {
|
if (posY == null) {
|
||||||
posY = new Integer(0);
|
posY = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the optional id of the parent PG, otherwise fallback to the root group
|
// 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.setFlowId(flowId);
|
||||||
versionControlInfo.setVersion(flowVersion);
|
versionControlInfo.setVersion(flowVersion);
|
||||||
|
|
||||||
|
PgBox pgBox = client.getFlowClient().getSuggestedProcessGroupCoordinates(parentPgId);
|
||||||
|
|
||||||
final PositionDTO posDto = new PositionDTO();
|
final PositionDTO posDto = new PositionDTO();
|
||||||
posDto.setX(posX.doubleValue());
|
posDto.setX(Integer.valueOf(pgBox.x).doubleValue());
|
||||||
posDto.setY(posY.doubleValue());
|
posDto.setY(Integer.valueOf(pgBox.y).doubleValue());
|
||||||
|
|
||||||
final ProcessGroupDTO pgDto = new ProcessGroupDTO();
|
final ProcessGroupDTO pgDto = new ProcessGroupDTO();
|
||||||
pgDto.setVersionControlInformation(versionControlInfo);
|
pgDto.setVersionControlInformation(versionControlInfo);
|
||||||
|
pgDto.setPosition(posDto);
|
||||||
|
|
||||||
final ProcessGroupEntity pgEntity = new ProcessGroupEntity();
|
final ProcessGroupEntity pgEntity = new ProcessGroupEntity();
|
||||||
pgEntity.setComponent(pgDto);
|
pgEntity.setComponent(pgDto);
|
||||||
|
|
Loading…
Reference in New Issue