azkaban-aplcache
Changes
src/java/azkaban/flow/Flow.java 16(+14 -2)
src/java/azkaban/flow/LayeredFlowLayout.java 400(+390 -10)
src/java/azkaban/flow/Node.java 31(+23 -8)
src/java/azkaban/utils/Utils.java 8(+8 -0)
src/web/css/azkaban.css 45(+43 -2)
src/web/js/azkaban.flow.view.js 83(+79 -4)
src/web/js/azkaban.project.view.js 2(+1 -1)
src/web/js/svgNavigate.js 350(+350 -0)
Details
src/java/azkaban/flow/Flow.java 16(+14 -2)
diff --git a/src/java/azkaban/flow/Flow.java b/src/java/azkaban/flow/Flow.java
index c0d2fba..4b657d9 100644
--- a/src/java/azkaban/flow/Flow.java
+++ b/src/java/azkaban/flow/Flow.java
@@ -13,8 +13,9 @@ public class Flow {
READY, RUNNING, RUNNING_WITH_FAILURE, FAILED, SUCCEEDED
}
private final String id;
- private ArrayList<Node> startNodes;
- private ArrayList<Node> endNodes;
+ private ArrayList<Node> startNodes = null;
+ private ArrayList<Node> endNodes = null;
+ private int numLevels = -1;
private HashMap<String, Node> nodes = new HashMap<String, Node>();
@@ -49,6 +50,7 @@ public class Flow {
for (Node node: startNodes) {
node.setLevel(0);
+ numLevels = 0;
recursiveSetLevels(node);
}
}
@@ -65,11 +67,16 @@ public class Flow {
// We pick whichever is higher to get the max distance from root.
int level = Math.max(node.getLevel() + 1, nextNode.getLevel());
nextNode.setLevel(level);
+ numLevels = Math.max(level, numLevels);
recursiveSetLevels(nextNode);
}
}
}
+ public int getNumLevels() {
+ return numLevels;
+ }
+
public List<Node> getStartNodes() {
return startNodes;
}
@@ -170,6 +177,7 @@ public class Flow {
flowObj.put("props", objectizeProperties());
flowObj.put("nodes", objectizeNodes());
flowObj.put("edges", objectizeEdges());
+ flowObj.put("layedout", isLayedOut);
if (errors != null) {
flowObj.put("errors", errors);
}
@@ -212,7 +220,11 @@ public class Flow {
Map<String, Object> flowObject = (Map<String,Object>)object;
String id = (String)flowObject.get("id");
+ Boolean layedout = (Boolean)flowObject.get("layedout");
Flow flow = new Flow(id);
+ if (layedout != null) {
+ flow.setLayedOut(layedout);
+ }
// Loading projects
List<Object> propertiesList = (List<Object>)flowObject.get("props");
src/java/azkaban/flow/LayeredFlowLayout.java 400(+390 -10)
diff --git a/src/java/azkaban/flow/LayeredFlowLayout.java b/src/java/azkaban/flow/LayeredFlowLayout.java
index 33b10b3..c72cea4 100644
--- a/src/java/azkaban/flow/LayeredFlowLayout.java
+++ b/src/java/azkaban/flow/LayeredFlowLayout.java
@@ -1,48 +1,428 @@
package azkaban.flow;
-import java.awt.geom.Point2D;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Random;
public class LayeredFlowLayout implements FlowLayout{
+ private static final double EPSILON = 0.000001;
+ private static final double MIN_X_SPACING = 1.0;
+ private static final double MIN_Y_SPACING = 1.0;
+
@Override
public void layoutFlow(Flow flow) {
- analyzeFlow(flow);
+ ArrayList<ArrayList<LayeredNode>> nodeLayers = setupFlow(flow);
+
+ int maxNodesInLevel = 0;
+ int levelWithMax = 0;
+ int index = 0;
+ for (ArrayList<LayeredNode> layer : nodeLayers) {
+
+ int num = layer.size();
+ if (num < maxNodesInLevel) {
+ maxNodesInLevel = num;
+ levelWithMax = index;
+ }
+ index++;
+ }
+
+ midUpDownScheme(nodeLayers, levelWithMax);
+
+ printLayer(flow.getId(), nodeLayers, maxNodesInLevel, levelWithMax);
flow.setLayedOut(true);
}
- private void analyzeFlow(Flow flow) {
- Map<String, Node> node = flow.getNodeMap();
+ private void assignPointsToFlowNodes(ArrayList<ArrayList<LayeredNode>> nodeLayers, double xScale, double minXSpacing) {
+ for (ArrayList<LayeredNode> layer: nodeLayers) {
+ for (LayeredNode lnode : layer) {
+ if (lnode instanceof WrappedNode) {
+ WrappedNode wnode = (WrappedNode)lnode;
+ Node node = wnode.getNode();
+ node.setPosition(wnode.getX()*xScale, lnode.level * minXSpacing);
+ }
+ }
+ }
+ }
+
+ // Lays out by the longest row item, up... then do all the whole thing.
+ private void midUpDownScheme(ArrayList<ArrayList<LayeredNode>> nodeLayers, int levelWithMax) {
+ LevelComparator comparator = new LevelComparator();
+
+ //ArrayList<LayeredNode> level = nodeLayers.get(levelWithMax);
+ ArrayList<LayeredNode> level = nodeLayers.get(nodeLayers.size() - 1);
+ Collections.sort(level, comparator);
+
+ Random random = new Random(1);
+ intializeLevel(level, random);
+ double min = Double.MAX_VALUE;
+
+ if (nodeLayers.size() > 2) {
+ // Going from the last item unwrapping upwards
+ min = Math.min(min, uncrossLayers(nodeLayers, nodeLayers.size() - 2, 0, comparator));
+
+ // Going from the first item unwrapping downward
+ min = Math.min(min, uncrossLayers(nodeLayers, 1, nodeLayers.size() - 1, comparator));
+ }
+ else if (nodeLayers.size() > 1) {
+ min = uncrossLayers(nodeLayers, 1, 1, comparator);
+ }
+
+ System.out.println("min:" + min);
+ double scale = min > 0 ? MIN_X_SPACING / min : MIN_X_SPACING;
+ scale = Math.max(scale, 1);
+ assignPointsToFlowNodes(nodeLayers, scale, MIN_Y_SPACING);
+ }
+
+ private void intializeLevel(ArrayList<LayeredNode> layers, Random random) {
+ double starting = 0;
+ for (LayeredNode node: layers) {
+ node.setX(starting);
+ node.setMaxX(starting + 0.5);
+ node.setMinX(starting - 0.5);
+
+ // Why random hopping? between 0.5 to 1.0
+ double randomHop = random.nextDouble();
+ starting += (1 + randomHop*0.5);
+ }
+ }
+
+ private double uncrossLayers(ArrayList<ArrayList<LayeredNode>> layers, int from, int to, LevelComparator comparator) {
+ double minDistance = Double.MAX_VALUE;
+
+ if (from < to) {
+ for (int i = from; i <= to; ++i) {
+ // Uncross layer
+ ArrayList<LayeredNode> layer = layers.get(i);
+ uncrossLayerFromIn(layer);
+ // Sort layer
+ Collections.sort(layer, comparator);
+ minDistance = Math.min(minDistance, separateLevels(layer));
+ }
+ }
+ else if (to < from){
+ for (int i = from; i >= to; --i) {
+ // Uncross layer
+ ArrayList<LayeredNode> layer = layers.get(i);
+ uncrossLayerFromOut(layer);
+ // Sort layer
+ Collections.sort(layer, comparator);
+ minDistance = Math.min(minDistance, separateLevels(layer));
+ }
+ }
+ else {
+ uncrossLayerFromIn(layers.get(from));
+ }
+
+ return minDistance;
+ }
+
+ private double separateLevels(ArrayList<LayeredNode> layer) {
+ int startIndex = -1;
+ int endIndex = -1;
+
+ if (layer.size() == 1) {
+ return Double.MAX_VALUE;
+ }
+
+ double xPrev = Double.NaN;
+ double xCurrent = Double.NaN;
+
+ double minDistance = Double.MAX_VALUE;
+
+ for (int i = 0; i < layer.size(); ++i) {
+ LayeredNode node = layer.get(i);
+ xCurrent = node.getX();
+ if (Double.isNaN(xPrev)) {
+ xPrev = xCurrent;
+ continue;
+ }
+
+ double delta = xCurrent - xPrev;
+ if (delta < EPSILON) {
+ if (startIndex == -1) {
+ startIndex = i - 1;
+ }
+ endIndex = i;
+ }
+ else {
+ if (startIndex != -1) {
+ minDistance = Math.min(separateRange(layer, startIndex, endIndex), minDistance);
+ // Reset
+ startIndex = -1;
+ endIndex = -1;
+ }
+ else {
+ minDistance = Math.min(delta, minDistance);
+ }
+ }
+
+ xPrev = xCurrent;
+ }
+
+ // Finish it off
+ if (startIndex != -1) {
+ minDistance = Math.min(separateRange(layer, startIndex, layer.size() - 1), minDistance);
+ }
+
+ return minDistance;
+ }
+
+ // Range is inclusive
+ private double separateRange(ArrayList<LayeredNode> layer, int startIndex, int endIndex) {
+ double startSplit = 0;
+ double endSplit = 0;
+ if (startIndex == 0) {
+ startSplit = layer.get(0).getMinX();
+ }
+ else {
+ startSplit = (layer.get(startIndex).getX() + layer.get(startIndex - 1).getX())/2.0;
+ }
+
+ if (endIndex == layer.size() - 1) {
+ endSplit = layer.get(endIndex).getMaxX();
+ }
+ else {
+ endSplit = (layer.get(endIndex + 1).getX() + layer.get(endIndex).getX())/2.0;
+ }
+
+ double deltaDiff = endSplit - startSplit;
+ if (deltaDiff < EPSILON) {
+ System.err.println("WTH It's 0!!");
+ }
+ else {
+ // startIndex - endIndex should be at least 2.
+ double step = deltaDiff / (double)(endIndex - startIndex);
+ double start = startSplit;
+ for (int i = startIndex; i <= endIndex; ++i) {
+ LayeredNode node = layer.get(i);
+ node.setX(start);
+ start += step;
+ }
+
+ return step;
+ }
+
+ return Double.NaN;
}
+ //Return the scale
+ private void uncrossLayerFromIn(ArrayList<LayeredNode> layer) {
+ for (LayeredNode node: layer) {
+ double xSum = 0;
+ double minX = Double.POSITIVE_INFINITY;
+ double maxX = Double.NEGATIVE_INFINITY;
+ int count = 0;
+
+ for (LayeredNode upperLayer : node.getInNode()) {
+ minX = Math.min(minX, upperLayer.getMinX());
+ maxX = Math.max(maxX, upperLayer.getMaxX());
+ xSum += upperLayer.getX();
+
+ count++;
+ }
+
+ if (count == 0) {
+ System.err.println("This is not right");
+ }
+ else {
+ double x = count == 0 ? 0 : xSum / (double)count;
+ node.setX(x);
+ node.setMaxX(maxX);
+ node.setMinX(minX);
+ }
+ }
+ }
+
+ private void uncrossLayerFromOut(ArrayList<LayeredNode> layer) {
+ for (LayeredNode node: layer) {
+ double xSum = 0;
+ double minX = Double.POSITIVE_INFINITY;
+ double maxX = Double.NEGATIVE_INFINITY;
+ int count = 0;
+
+
+ for (LayeredNode lowerLayer : node.getOutNode()) {
+ minX = Math.min(minX, lowerLayer.getMinX());
+ maxX = Math.max(maxX, lowerLayer.getMaxX());
+ xSum += lowerLayer.getX();
+
+ count++;
+ }
+
+ if (count == 0) {
+ System.err.println("This is not right");
+ }
+ else {
+ double x = xSum / (double)count;
+ node.setX(x);
+ node.setMaxX(maxX);
+ node.setMinX(minX);
+ }
+ }
+ }
+
+ private ArrayList<ArrayList<LayeredNode>> setupFlow(Flow flow) {
+ Map<String, Node> nodesMap = flow.getNodeMap();
+ int levels = flow.getNumLevels();
+
+ ArrayList<ArrayList<LayeredNode>> nodeLevels = new ArrayList<ArrayList<LayeredNode>>();
+ for (int i = 0; i < levels + 1; ++i) {
+ nodeLevels.add(new ArrayList<LayeredNode>());
+ }
+
+ HashMap<String, WrappedNode> layeredNodeMap = new HashMap<String, WrappedNode>();
+ for (Node node: nodesMap.values()) {
+ int level = node.getLevel();
+ WrappedNode wNode = new WrappedNode(node);
+ layeredNodeMap.put(node.getId(), wNode);
+
+ ArrayList<LayeredNode> nodeList = nodeLevels.get(level);
+ nodeList.add(wNode);
+ }
+
+ // Adding edges and dummy nodes.
+ for(Edge edge : flow.getEdges()) {
+ if (edge.hasError()) {
+ continue;
+ }
+
+ LayeredNode source = layeredNodeMap.get(edge.getSourceId());
+ LayeredNode dest = layeredNodeMap.get(edge.getTargetId());
+ int sourceLevel = source.getLevel();
+ int destLevel = source.getLevel();
+
+ for (int index = sourceLevel + 1; index < destLevel; index++) {
+ LayeredNode dummyNode = new LayeredNode();
+ dummyNode.setLevel(index);
+ ArrayList<LayeredNode> nodeList = nodeLevels.get(index);
+ nodeList.add(dummyNode);
+
+ source.addOutNode(dummyNode);
+ dummyNode.addInNode(source);
+ source = dummyNode;
+ }
+ source.addOutNode(dest);
+ dest.addInNode(source);
+ }
+
+ return nodeLevels;
+ }
+
private class WrappedNode extends LayeredNode {
private Node node;
public WrappedNode(Node node) {
this.node = node;
+ super.level = node.getLevel();
}
public Node getNode() {
return node;
}
+ @Override
+ public String getId() {
+ return node.getId();
+ }
}
private class LayeredNode {
- private Point2D point;
private int level;
private ArrayList<LayeredNode> inNodes;
private ArrayList<LayeredNode> outNodes;
-
+ private double minX;
+ private double maxX;
+ private double x;
+ private double y;
+
+ public LayeredNode() {
+ inNodes = new ArrayList<LayeredNode>();
+ outNodes = new ArrayList<LayeredNode>();
+ }
+
+ public String getId() {
+ return "dummy";
+ }
+
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
- public Point2D getPoint() {
- return point;
+ public double getX() {
+ return x;
+ }
+ public void setX(double x) {
+ this.x = x;
+ }
+ public double getMinX() {
+ return minX;
+ }
+ public void setMinX(double min) {
+ minX = min;
+ }
+ public double getMaxX() {
+ return maxX;
+ }
+ public void setMaxX(double max) {
+ this.maxX = max;
}
- public void setPoint(Point2D point) {
- this.point = point;
+ public void setY(double y) {
+ this.y = y;
+ }
+ public double getY() {
+ return y;
+ }
+
+ public void addInNode(LayeredNode node) {
+ inNodes.add(node);
+ }
+
+ public void addOutNode(LayeredNode node) {
+ outNodes.add(node);
+ }
+
+ public List<LayeredNode> getInNode() {
+ return inNodes;
+ }
+
+ public List<LayeredNode> getOutNode() {
+ return outNodes;
+ }
+ }
+
+ private void printLayer(String flowName, ArrayList<ArrayList<LayeredNode>> nodeLayers, int maxNodesInLevel, int levelWithMax) {
+ System.out.println("Layout " + flowName);
+ System.out.println(" Max Width " + maxNodesInLevel);
+ System.out.println(" Level with max " + levelWithMax);
+ int index = 0;
+ for (ArrayList<LayeredNode> lnodes: nodeLayers) {
+ StringBuffer nodeStr = new StringBuffer();
+ nodeStr.append(" ");
+ nodeStr.append(index);
+ nodeStr.append(" [");
+ for (LayeredNode node: lnodes) {
+ nodeStr.append(node.getId());
+ nodeStr.append(",");
+ }
+ nodeStr.append("]");
+ index++;
+ System.out.println(nodeStr);
+ }
+
+ }
+
+ private class LevelComparator implements Comparator<LayeredNode> {
+
+ @Override
+ public int compare(LayeredNode o1, LayeredNode o2) {
+ Double x1 = o1.getX();
+ Double x2 = o2.getX();
+
+ return x1.compareTo(x2);
}
}
}
src/java/azkaban/flow/Node.java 31(+23 -8)
diff --git a/src/java/azkaban/flow/Node.java b/src/java/azkaban/flow/Node.java
index 97320e4..bf37599 100644
--- a/src/java/azkaban/flow/Node.java
+++ b/src/java/azkaban/flow/Node.java
@@ -4,6 +4,8 @@ import java.awt.geom.Point2D;
import java.util.HashMap;
import java.util.Map;
+import azkaban.utils.Utils;
+
public class Node {
public enum State {
FAILED, SUCCEEDED, RUNNING, WAITING, IGNORED
@@ -14,7 +16,7 @@ public class Node {
private String propsSource;
private State state = State.WAITING;
- private Point2D.Double position = null;
+ private Point2D position = null;
private int level;
private int expectedRunTimeSec = 1;
@@ -45,14 +47,18 @@ public class Node {
this.state = state;
}
- public Point2D.Double getPosition() {
+ public Point2D getPosition() {
return position;
}
- public void setPosition(Point2D.Double position) {
+ public void setPosition(Point2D position) {
this.position = position;
}
+ public void setPosition(double x, double y) {
+ this.position = new Point2D.Double(x,y);
+ }
+
public int getLevel() {
return level;
}
@@ -111,9 +117,18 @@ public class Node {
Map<String,Object> layoutInfo = (Map<String,Object>)mapObj.get("layout");
if (layoutInfo != null) {
- Double x = (Double)layoutInfo.get("x");
- Double y = (Double)layoutInfo.get("y");
- Integer level = (Integer)layoutInfo.get("level");
+ Double x = null;
+ Double y = null;
+ Integer level = null;
+
+ try {
+ x = Utils.convertToDouble(layoutInfo.get("x"));
+ y = Utils.convertToDouble(layoutInfo.get("y"));
+ level = (Integer)layoutInfo.get("level");
+ }
+ catch (ClassCastException e) {
+ throw new RuntimeException("Error creating node " + id, e);
+ }
if (x != null && y != null) {
node.setPosition(new Point2D.Double(x, y));
@@ -136,8 +151,8 @@ public class Node {
HashMap<String, Object> layoutInfo = new HashMap<String, Object>();
if (position != null) {
- layoutInfo.put("x", position.x);
- layoutInfo.put("y", position.y);
+ layoutInfo.put("x", position.getX());
+ layoutInfo.put("y", position.getY());
}
layoutInfo.put("level", level);
objMap.put("layout", layoutInfo);
diff --git a/src/java/azkaban/project/FileProjectManager.java b/src/java/azkaban/project/FileProjectManager.java
index 041d90f..22b7efc 100644
--- a/src/java/azkaban/project/FileProjectManager.java
+++ b/src/java/azkaban/project/FileProjectManager.java
@@ -16,6 +16,7 @@ import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import azkaban.flow.Flow;
+import azkaban.flow.LayeredFlowLayout;
import azkaban.user.Permission;
import azkaban.user.Permission.Type;
import azkaban.user.User;
@@ -108,13 +109,33 @@ public class FileProjectManager implements ProjectManager {
try {
objectizedFlow = JSONUtils.parseJSONFromFile(flowFile);
} catch (IOException e) {
- logger.error("Error parsing flow file " + flowFile.toString());
+ logger.error("Error parsing flow file " + flowFile.toString(), e);
+ continue;
}
//Recreate Flow
- Flow flow = Flow.flowFromObject(objectizedFlow);
+ Flow flow = null;
+
+ try {
+ flow = Flow.flowFromObject(objectizedFlow);
+ }
+ catch (Exception e) {
+ logger.error("Error loading flow " + flowFile.getName() + " in project " + project.getName(), e);
+ continue;
+ }
logger.debug("Loaded flow " + project.getName() + ": " + flow.getId());
flow.initialize();
+ //if (!flow.isLayedOut()) {
+ LayeredFlowLayout layout = new LayeredFlowLayout();
+ layout.layoutFlow(flow);
+
+ try {
+ writeFlowFile(flowFile.getParentFile(), flow);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ //}
+
flowMap.put(flow.getId(), flow);
}
@@ -183,11 +204,13 @@ public class FileProjectManager implements ProjectManager {
if (flow.getErrors() != null) {
errors.addAll(flow.getErrors());
}
+ flow.initialize();
+ LayeredFlowLayout layout = new LayeredFlowLayout();
+ layout.layoutFlow(flow);
writeFlowFile(installDir, flow);
} catch (IOException e) {
throw new ProjectManagerException(
- "Project directory " + projectName +
- " cannot be created in " + projectDirectory, e);
+ "Project directory " + projectName + " cannot be created in " + projectDirectory, e);
}
}
@@ -310,6 +333,12 @@ public class FileProjectManager implements ProjectManager {
Object object = flow.toObject();
String filename = flow.getId() + FLOW_EXTENSION;
File outputFile = new File(directory, filename);
+ File oldOutputFile = new File(directory, filename + ".old");
+
+ if (outputFile.exists()) {
+ outputFile.renameTo(oldOutputFile);
+ }
+
logger.info("Writing flow file " + outputFile);
String output = JSONUtils.toJSON(object, true);
@@ -324,6 +353,10 @@ public class FileProjectManager implements ProjectManager {
throw e;
}
writer.close();
+
+ if (oldOutputFile.exists()) {
+ oldOutputFile.delete();
+ }
}
@Override
src/java/azkaban/utils/Utils.java 8(+8 -0)
diff --git a/src/java/azkaban/utils/Utils.java b/src/java/azkaban/utils/Utils.java
index 1d22bb7..5d95084 100644
--- a/src/java/azkaban/utils/Utils.java
+++ b/src/java/azkaban/utils/Utils.java
@@ -160,4 +160,12 @@ public class Utils {
}
return buffer.toString();
}
+
+ public static Double convertToDouble(Object obj) {
+ if (obj instanceof String) {
+ return Double.parseDouble((String)obj);
+ }
+
+ return (Double)obj;
+ }
}
\ No newline at end of file
diff --git a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
index e11e39e..986a8e7 100644
--- a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
@@ -121,47 +121,90 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
}
String jsonName = getParam(req, "json");
- if (jsonName.equals("expandflow")) {
- String flowId = getParam(req, "flow");
- Flow flow = project.getFlow(flowId);
+ if (jsonName.equals("fetchflowlist")) {
+ jsonFetchFlow(project, ret, req, resp);
+ }
+ else if (jsonName.equals("fetchflowgraph")) {
+ jsonFetchFlowGraph(project, ret, req, resp);
+ }
+
+ this.writeJSON(resp, ret);
+ }
+
+ private void jsonFetchFlowGraph(Project project, HashMap<String, Object> ret, HttpServletRequest req, HttpServletResponse resp) throws ServletException {
+ String flowId = getParam(req, "flow");
+ Flow flow = project.getFlow(flowId);
+
+ //Collections.sort(flowNodes, NODE_LEVEL_COMPARATOR);
+ ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
+ for (Node node: flow.getNodes()) {
+ HashMap<String, Object> nodeObj = new HashMap<String,Object>();
+ nodeObj.put("id", node.getId());
+ nodeObj.put("x", node.getPosition().getX());
+ nodeObj.put("y", node.getPosition().getY());
+ if (node.getState() != Node.State.WAITING) {
+ nodeObj.put("state", node.getState());
+ }
+ nodeList.add(nodeObj);
+ }
+
+ ArrayList<Map<String, Object>> edgeList = new ArrayList<Map<String, Object>>();
+ for (Edge edge: flow.getEdges()) {
+ HashMap<String, Object> edgeObj = new HashMap<String,Object>();
+ edgeObj.put("from", edge.getSourceId());
+ edgeObj.put("target", edge.getTargetId());
+
+ if (edge.hasError()) {
+ edgeObj.put("error", edge.getError());
+ }
+
+ edgeList.add(edgeObj);
+ }
+
+ ret.put("flowId", flowId);
+ ret.put("nodes", nodeList);
+ ret.put("edges", edgeList);
+ }
+
+ private void jsonFetchFlow(Project project, HashMap<String, Object> ret, HttpServletRequest req, HttpServletResponse resp) throws ServletException {
+ String flowId = getParam(req, "flow");
+ Flow flow = project.getFlow(flowId);
- ArrayList<Node> flowNodes = new ArrayList<Node>(flow.getNodes());
- Collections.sort(flowNodes, NODE_LEVEL_COMPARATOR);
+ ArrayList<Node> flowNodes = new ArrayList<Node>(flow.getNodes());
+ Collections.sort(flowNodes, NODE_LEVEL_COMPARATOR);
- ArrayList<Object> nodeList = new ArrayList<Object>();
- for (Node node: flowNodes) {
- HashMap<String, Object> nodeObj = new HashMap<String, Object>();
- nodeObj.put("id", node.getId());
-
- ArrayList<String> dependencies = new ArrayList<String>();
- Collection<Edge> collection = flow.getInEdges(node.getId());
- if (collection != null) {
- for (Edge edge: collection) {
- dependencies.add(edge.getSourceId());
- }
+ ArrayList<Object> nodeList = new ArrayList<Object>();
+ for (Node node: flowNodes) {
+ HashMap<String, Object> nodeObj = new HashMap<String, Object>();
+ nodeObj.put("id", node.getId());
+
+ ArrayList<String> dependencies = new ArrayList<String>();
+ Collection<Edge> collection = flow.getInEdges(node.getId());
+ if (collection != null) {
+ for (Edge edge: collection) {
+ dependencies.add(edge.getSourceId());
}
-
- ArrayList<String> dependents = new ArrayList<String>();
- collection = flow.getOutEdges(node.getId());
- if (collection != null) {
- for (Edge edge: collection) {
- dependents.add(edge.getTargetId());
- }
+ }
+
+ ArrayList<String> dependents = new ArrayList<String>();
+ collection = flow.getOutEdges(node.getId());
+ if (collection != null) {
+ for (Edge edge: collection) {
+ dependents.add(edge.getTargetId());
}
-
- nodeObj.put("dependencies", dependencies);
- nodeObj.put("dependents", dependents);
- nodeObj.put("level", node.getLevel());
- nodeList.add(nodeObj);
}
- ret.put("flowId", flowId);
- ret.put("nodes", nodeList);
+ nodeObj.put("dependencies", dependencies);
+ nodeObj.put("dependents", dependents);
+ nodeObj.put("level", node.getLevel());
+ nodeList.add(nodeObj);
}
- this.writeJSON(resp, ret);
+ ret.put("flowId", flowId);
+ ret.put("nodes", nodeList);
}
+
private void handleFlowPage(HttpServletRequest req, HttpServletResponse resp, Session session) throws ServletException {
Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/flowpage.vm");
String projectName = getParam(req, "project");
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index 145d820..6d3f7bd 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -10,6 +10,7 @@
<script type="text/javascript" src="${context}/js/jquery.simplemodal.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.nav.js"></script>
<script type="text/javascript" src="${context}/js/azkaban.flow.view.js"></script>
+ <script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
<script type="text/javascript">
var contextURL = "${context}";
var currentTime = ${currentTime};
@@ -42,7 +43,7 @@
<h3><a href="${context}/manager?project=${project.name}&flow=${flowid}">Flow <span>$flowid</span></a></h3>
</div>
- <div class="headertabs">
+ <div id="headertabs" class="headertabs">
<ul>
<li><a id="graphViewLink" href="#graph">Graph</a></li>
<li class="lidivider">|</li>
@@ -50,7 +51,15 @@
</ul>
</div>
<div id="graphView">
- <p>This is my graph view</p>
+ <div class="relative">
+ <div id="jobList">
+ Loading Flow ${flowid}
+ </div>
+ <div id="svgDiv" >
+ <svg id="svgGraph" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100% 100%" >
+ </svg>
+ </div>
+ </div>
</div>
<div id="jobListView">
<p>This is my joblist view</p>
src/web/css/azkaban.css 45(+43 -2)
diff --git a/src/web/css/azkaban.css b/src/web/css/azkaban.css
index ee55776..0b640e8 100644
--- a/src/web/css/azkaban.css
+++ b/src/web/css/azkaban.css
@@ -113,7 +113,8 @@ textarea {
.content {
background-color: #E0E0E0;
border: 1px solid #cdcdcd;
- margin: 0 4.75% 10px;
+ margin: 0 50px 10px;
+ min-height: 240px;
}
.section-ft {
@@ -899,7 +900,7 @@ tr:hover td {
}
.headertabs {
- height: 24px;
+ height: 26px;
width:100%;
background-image: -o-linear-gradient(bottom, rgb(56,56,56) 33%, rgb(73,73,73) 66%);
background-image: -moz-linear-gradient(bottom, rgb(56,56,56) 33%, rgb(73,73,73) 66%);
@@ -951,6 +952,46 @@ tr:hover td {
clear: both;
}
+/* Graph SVG */
+#graphView {
+ position: absolute;
+ top: 210px;
+ bottom: 5px;
+ left: 50px;
+ right: 50px;
+ padding: 10px;
+ background: #E0E0E0;
+}
+
+.relative {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+#svgGraph {
+ width: 100%;
+ height: 100%;
+ background: #fff;
+}
+
+#svgDiv {
+ position: absolute;
+ top: 0px;
+ right: 0px;
+ left: 260px;
+ bottom: 0px;
+ height: 100%;
+}
+
+#jobList {
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ height: 100%;
+ width: 260px;
+}
+
/* old styles */
.azkaban-charts .hitarea {
src/web/js/azkaban.flow.view.js 83(+79 -4)
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index 596f5f5..5129e40 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -1,7 +1,7 @@
$.namespace('azkaban');
-var flowView;
-azkaban.FlowView= Backbone.View.extend({
+var flowTabView;
+azkaban.FlowTabView= Backbone.View.extend({
events : {
"click #graphViewLink" : "handleGraphLinkClick",
"click #jobslistViewLink" : "handleJobslistLinkClick"
@@ -17,7 +17,7 @@ azkaban.FlowView= Backbone.View.extend({
},
render: function() {
-
+ console.log("render graph");
},
handleGraphLinkClick: function(){
$("#jobslistViewLink").removeClass("selected");
@@ -35,6 +35,65 @@ azkaban.FlowView= Backbone.View.extend({
}
});
+var svgGraphView;
+azkaban.SvgGraphView = Backbone.View.extend({
+ events: {
+ },
+ initialize: function(settings) {
+ this.model.bind('change:selected', this.changeSelected, this);
+ this.model.bind('change:graph', this.render, this);
+
+ this.svgns = "http://www.w3.org/2000/svg";
+ this.xlinksn = "http://www.w3.org/1999/xlink";
+
+ var graphDiv = this.el[0];
+ var svg = $('#svgGraph')[0];
+ this.svgGraph = svg;
+
+ var gNode = document.createElementNS(this.svgns, 'g');
+ gNode.setAttribute("id", "group");
+ svg.appendChild(gNode);
+ this.mainG = gNode;
+
+ $(svg).svgNavigate();
+ },
+ render: function(self) {
+ console.log("graph render");
+
+ var data = this.model.get("data");
+ var nodes = data.nodes;
+ for (var i = 0; i < nodes.length; ++i) {
+ this.drawNode(this, nodes[i]);
+ }
+ },
+ changeSelected: function(self) {
+ console.log("change selected");
+ },
+ drawNode: function(self, node) {
+ var svg = self.svgGraph;
+ var svgns = self.svgns;
+
+ var nodeG = document.createElementNS(svgns, "g");
+ nodeG.setAttributeNS(null, "id", node.id);
+ nodeG.setAttributeNS(null, "font-family", "helvetica");
+ nodeG.setAttributeNS(null, "transform", "translate(" + (node.x * 100) + "," + (node.y*100)+ ")");
+
+ var rect1 = document.createElementNS(svgns, 'rect');
+ rect1.setAttributeNS(null, "y", 2);
+ rect1.setAttributeNS(null, "x", 2);
+ rect1.setAttributeNS(null, "ry", 12);
+ rect1.setAttributeNS(null, "width", 20);
+ rect1.setAttributeNS(null, "height", 30);
+ rect1.setAttributeNS(null, "style", "width:inherit;fill-opacity:1.0;stroke-opacity:1");
+
+ nodeG.appendChild(rect1);
+ self.mainG.appendChild(nodeG);
+ }
+});
+
+var graphModel;
+azkaban.GraphModel = Backbone.Model.extend({});
+
$(function() {
var selected;
if (window.location.hash) {
@@ -50,5 +109,21 @@ $(function() {
selected = "graph";
}
}
- flowView = new azkaban.FlowView({el:$( '#all-jobs-content'), selectedView: selected });
+ flowTabView = new azkaban.FlowTabView({el:$( '#headertabs'), selectedView: selected });
+
+ graphModel = new azkaban.GraphModel();
+ svgGraphView = new azkaban.SvgGraphView({el:$('#svgDiv'), model: graphModel});
+
+ var requestURL = contextURL + "/manager";
+
+ $.get(
+ requestURL,
+ {"project": projectName, "json":"fetchflowgraph", "flow":flowName},
+ function(data) {
+ console.log("data fetched");
+ graphModel.set({data: data});
+ graphModel.trigger("change:graph");
+ },
+ "json"
+ );
});
src/web/js/azkaban.project.view.js 2(+1 -1)
diff --git a/src/web/js/azkaban.project.view.js b/src/web/js/azkaban.project.view.js
index 6616e4a..76dee8d 100644
--- a/src/web/js/azkaban.project.view.js
+++ b/src/web/js/azkaban.project.view.js
@@ -82,7 +82,7 @@ azkaban.FlowTableView= Backbone.View.extend({
$.get(
requestURL,
- {"project": projectId, "json":"expandflow", "flow":targetId},
+ {"project": projectId, "json":"fetchflowlist", "flow":targetId},
function(data) {
console.log("Success");
target.loaded = true;
src/web/js/svgNavigate.js 350(+350 -0)
diff --git a/src/web/js/svgNavigate.js b/src/web/js/svgNavigate.js
new file mode 100644
index 0000000..71f1915
--- /dev/null
+++ b/src/web/js/svgNavigate.js
@@ -0,0 +1,350 @@
+(function($) {
+
+ var mouseUp = function(evt) {
+ var target = evt.target;
+ target.mx = evt.clientX;
+ target.my = evt.clientY;
+ target.mDown = false;
+ }
+
+ var mouseDown = function(evt) {
+ //alert("mouseDown");
+ var target = evt.target;
+ target.mx = evt.clientX;
+ target.my = evt.clientY;
+ target.mDown = true;
+ }
+
+ var mouseOut = function(evt) {
+ var target = evt.target;
+ target.mx = evt.clientX;
+ target.my = evt.clientY;
+ target.mDown = false;
+ }
+
+ var mouseMove = function(evt) {
+ var target = evt.target;
+ if (target.mDown) {
+ var dx = evt.clientX - target.mx;
+ var dy = evt.clientY - target.my;
+
+ evt.dragX = dx;
+ evt.dragY = dy;
+ mouseDrag(evt);
+ }
+
+ target.mx = evt.clientX;
+ target.my = evt.clientY;
+ }
+
+ var mouseDrag = function(evt) {
+ //alert("mouseDragged ");
+ translateDeltaGraph(evt.target, evt.dragX, evt.dragY);
+ }
+
+ var mouseScrolled = function(evt) {
+ //alert("scroll");
+ if (!evt) {
+ evt = window.event;
+ }
+ var target = evt.target;
+ var leftOffset = 0;
+ var topOffset = 0;
+ if (!target.marker) {
+ while (!target.farthestViewportElement) {
+ target = target.parentNode;
+ }
+
+ target = target.farthestViewportElement;
+ }
+
+ if (target.parentNode.offsetLeft) {
+ leftOffset = target.parentNode.offsetLeft;
+ }
+ if (target.parentNode.offsetTop) {
+ topOffset = target.parentNode.offsetTop;
+ }
+
+ // Trackball/trackpad vs wheel. Need to accommodate
+ var delta = 0;
+ if (evt.wheelDelta) {
+ delta = event.wheelDelta / 120;
+ }
+ else if (evt.detail) {
+ delta = -evt.detail / 3;
+ }
+
+ var zoomLevel = boundZoomLevel(target, Math.floor(target.zoomIndex + delta));
+ target.zoomIndex = zoomLevel;
+ var scale = target.zoomLevels[zoomLevel];
+
+ var x = evt.clientX - leftOffset;
+ var y = evt.clientY - topOffset;
+
+ scaleGraph(target, scale, x, y);
+ }
+
+ this.boundZoomLevel = function(target, level) {
+ if (level >= target.settings.zoomNumLevels) {
+ return target.settings.zoomNumLevels - 1;
+ }
+ else if (level <= 0 ) {
+ return 0;
+ }
+
+ return level;
+ }
+
+ this.scaleGraph = function(target, scale, x, y) {
+ var sfactor = scale/target.scale;
+ target.scale = scale;
+
+ target.translateX = sfactor*target.translateX + x - sfactor*x;
+ target.translateY = sfactor*target.translateY + y - sfactor*y;
+
+ if (target.model) {
+ target.model.trigger("scaled");
+ }
+ retransform(target);
+ }
+
+ this.translateDeltaGraph = function(target, x, y) {
+ target.translateX += x;
+ target.translateY += y;
+ if (target.model) {
+ target.model.trigger("panned");
+ }
+ retransform(target);
+ }
+
+ this.retransform = function(target) {
+ //$(target).children('g').attr("transform", "translate(" + target.translateX + "," + target.translateY + ") scale(" + target.scale + ")");
+ var gs = target.childNodes;
+
+ var transformString = "translate(" + target.translateX + "," + target.translateY + ") scale(" + target.scale + ")";
+ for (var i = 0; i < gs.length; ++i) {
+ var g = gs[i];
+ if (g.nodeName == 'g') {
+ g.setAttribute("transform", transformString);
+ }
+ }
+
+ if (target.model) {
+ var obj = target.model.get("transform");
+ if (obj) {
+ obj.scale = target.scale;
+ obj.height = target.parentNode.clientHeight;
+ obj.width = target.parentNode.clientWidth;
+
+ obj.x1 = target.translateX;
+ obj.y1 = target.translateY;
+ obj.x2 = obj.x1 + obj.width*obj.scale;
+ obj.y2 = obj.y1 + obj.height*obj.scale;
+ }
+ }
+ }
+
+ this.resetTransform = function(target) {
+ var settings = target.settings;
+ target.translateX = settings.x;
+ target.translateY = settings.y;
+
+ if (settings.x < settings.x2) {
+ var factor = 0.90;
+
+ // Reset scale and stuff.
+ var divHeight = target.parentNode.clientHeight;
+ var divWidth = target.parentNode.clientWidth;
+
+ var width = settings.x2 - settings.x;
+ var height = settings.y2 - settings.y;
+ var aspectRatioGraph = height/width;
+ var aspectRatioDiv = divHeight/divWidth;
+
+ var scale = aspectRatioGraph > aspectRatioDiv ? (divHeight/height)*factor : (divWidth/width)*factor;
+ target.scale = scale;
+ }
+ else {
+ target.zoomIndex = boundZoomLevel(target, settings.zoomIndex);
+ target.scale = target.zoomLevels[target.zoomIndex];
+ }
+ }
+
+ this.animateTransform = function(target, scale, x, y, duration) {
+ var zoomLevel = calculateZoomLevel(scale, target.zoomLevels);
+ target.fromScaleLevel = target.zoomIndex;
+ target.toScaleLevel = zoomLevel;
+ target.fromX = target.translateX;
+ target.fromY = target.translateY;
+ target.fromScale = target.scale;
+ target.toScale = target.zoomLevels[zoomLevel];
+ target.toX = x;
+ target.toY = y;
+ target.startTime = new Date().getTime();
+ target.endTime = target.startTime + duration;
+
+ this.animateTick(target);
+ }
+
+ this.animateTick = function(target) {
+ var time = new Date().getTime();
+ if (time < target.endTime) {
+ var timeDiff = time - target.startTime;
+ var progress = timeDiff / (target.endTime - target.startTime);
+
+ //target.zoomIndex = Math.floor((target.toScaleLevel - target.fromScaleLevel)*progress) + target.fromScaleLevel;
+ //target.scale = target.zoomLevels[target.zoomIndex];
+ target.scale = (target.toScale - target.fromScale)*progress + target.fromScale;
+ target.translateX = (target.toX - target.fromX)*progress + target.fromX;
+ target.translateY = (target.toY - target.fromY)*progress + target.fromY;
+ retransform(target);
+ setTimeout(function() {this.animateTick(target)}, 1);
+ }
+ else {
+ target.zoomIndex = target.toScaleLevel;
+ target.scale = target.zoomLevels[target.zoomIndex];
+ target.translateX = target.toX;
+ target.translateY = target.toY;
+ retransform(target);
+ }
+ }
+
+ this.calculateZoomScale = function(scaleLevel, numLevels, points) {
+ if (scaleLevel <= 0) {
+ return points[0];
+ }
+ else if (scaleLevel >= numLevels) {
+ return points[points.length - 1];
+ }
+ var factor = (scaleLevel / numLevels) * (points.length - 1);
+ var floorIdx = Math.floor(factor);
+ var ceilingIdx = Math.ceil(factor);
+
+ var b = factor - floorIdx;
+
+ return b*(points[ceilingIdx] - points[floorIdx]) + points[floorIdx];
+ }
+
+ this.calculateZoomLevel = function(scale, zoomLevels) {
+ if (scale >= zoomLevels[zoomLevels.length - 1]) {
+ return zoomLevels.length - 1;
+ }
+ else if (scale <= zoomLevels[0]) {
+ return 0;
+ }
+
+ var i = 0;
+ // Plain old linear scan
+ for (; i < zoomLevels.length; ++i) {
+ if (scale < zoomLevels[i]) {
+ i--;
+ break;
+ }
+ }
+
+ if (i < 0) {
+ return 0;
+ }
+
+ return i;
+ }
+
+ var methods = {
+ init : function(options) {
+ var settings = {
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 0,
+ minX: -1000,
+ minY: -1000,
+ maxX: 1000,
+ maxY: 1000,
+ zoomIndex: 24,
+ zoomPoints: [0.1, 0.14, 0.2, 0.4, 0.8, 1, 1.6, 2.4, 4, 8, 16],
+ zoomNumLevels: 48
+ };
+ if (options) {
+ $.extend(settings, options);
+ }
+ return this.each(function() {
+ var $this = $(this);
+ this.settings = settings;
+ this.marker = true;
+
+ if (window.addEventListener) this.addEventListener('DOMMouseScroll', mouseScrolled, false);
+ this.onmousewheel = mouseScrolled;
+ this.onmousedown = mouseDown;
+ this.onmouseup = mouseUp;
+ this.onmousemove = mouseMove;
+ this.onmouseout = mouseOut;
+
+ this.zoomLevels = new Array(settings.zoomNumLevels);
+ for (var i = 0; i < settings.zoomNumLevels; ++i) {
+ var scale = calculateZoomScale(i, settings.zoomNumLevels, settings.zoomPoints);
+ this.zoomLevels[i] = scale;
+ }
+ resetTransform(this);
+ });
+ },
+ transformToBox : function(arguments) {
+ var $this = $(this);
+ var target = ($this)[0];
+ var x = arguments.x;
+ var y = arguments.y;
+ var factor = 0.9;
+
+ var width = arguments.width ? arguments.width : 1;
+ var height = arguments.height ? arguments.height : 1;
+
+ var divHeight = target.parentNode.clientHeight;
+ var divWidth = target.parentNode.clientWidth;
+
+ var aspectRatioGraph = height/width;
+ var aspectRatioDiv = divHeight/divWidth;
+
+ var scale = aspectRatioGraph > aspectRatioDiv ? (divHeight/height)*factor : (divWidth/width)*factor;
+ console.log("(" + x + "," + y + "," + width.toPrecision(4) + "," + height.toPrecision(4) + ")");
+ console.log("(rg:" + aspectRatioGraph.toPrecision(3) + ",rd:" + aspectRatioDiv.toPrecision(3) + "," + scale.toPrecision(3) + ")");
+
+ // Center
+ var scaledWidth = width*scale;
+ var scaledHeight = height*scale;
+
+ var sx = (divWidth - scaledWidth)/2 -scale*x;
+ var sy = (divHeight - scaledHeight)/2 -scale*y;
+ console.log("sx,sy:" + sx + "," + sy);
+ animateTransform(target, scale, sx, sy, 500);
+ },
+ attachNavigateModel : function(arguments) {
+ var $this = $(this);
+ var target = ($this)[0];
+ target.model = arguments;
+
+ if (target.model) {
+ var obj = {};
+ obj.scale = target.scale;
+ obj.height = target.parentNode.clientHeight;
+ obj.width = target.parentNode.clientWidth;
+
+ obj.x1 = target.translateX;
+ obj.y1 = target.translateY;
+ obj.x2 = obj.x1 + obj.height*obj.scale;
+ obj.y2 = obj.y1 + obj.width*obj.scale;
+
+ target.model.set({transform : obj});
+ }
+ }
+ };
+
+ // Main Constructor
+ $.fn.svgNavigate = function(method) {
+ if (methods[method]) {
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+ } else if (typeof method === 'object' || !method) {
+ return methods.init.apply(this, arguments);
+ } else {
+ $.error('Method ' + method + ' does not exist on svgNavigate');
+ }
+ };
+})(jQuery);
\ No newline at end of file