azkaban-developers

Re-wiring of the Flow Execution viewer. Still no wiring prepare

1/14/2014 3:54:52 AM

Details

diff --git a/src/java/azkaban/execapp/ExecutorServlet.java b/src/java/azkaban/execapp/ExecutorServlet.java
index 95ca583..94fb4c2 100644
--- a/src/java/azkaban/execapp/ExecutorServlet.java
+++ b/src/java/azkaban/execapp/ExecutorServlet.java
@@ -57,8 +57,7 @@ public class ExecutorServlet extends HttpServlet implements ConnectorParams {
 		application = (AzkabanExecutorServer) config.getServletContext().getAttribute(AzkabanServletContextListener.AZKABAN_SERVLET_CONTEXT_KEY);
 
 		if (application == null) {
-			throw new IllegalStateException(
-					"No batch application is defined in the servlet context!");
+			throw new IllegalStateException("No batch application is defined in the servlet context!");
 		}
 
 		flowRunnerManager = application.getFlowRunnerManager();
diff --git a/src/java/azkaban/execapp/JobRunner.java b/src/java/azkaban/execapp/JobRunner.java
index 12805c3..912844e 100644
--- a/src/java/azkaban/execapp/JobRunner.java
+++ b/src/java/azkaban/execapp/JobRunner.java
@@ -471,7 +471,7 @@ public class JobRunner extends EventHandler implements Runnable {
 				job = jobtypeManager.buildJobExecutor(this.jobId, props, logger);
 			}
 			catch (JobTypeManagerException e) {
-				logger.error("Failed to build job type, skipping this job");
+				logger.error("Failed to build job type");
 				return false;
 			}
 		}
diff --git a/src/java/azkaban/executor/ExecutionOptions.java b/src/java/azkaban/executor/ExecutionOptions.java
index d5ecc7f..7913751 100644
--- a/src/java/azkaban/executor/ExecutionOptions.java
+++ b/src/java/azkaban/executor/ExecutionOptions.java
@@ -20,10 +20,8 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 import azkaban.executor.mail.DefaultMailCreator;
 import azkaban.utils.TypedMapWrapper;
@@ -73,7 +71,7 @@ public class ExecutionOptions {
 	
 	private FailureAction failureAction = FailureAction.FINISH_CURRENTLY_RUNNING;
 	
-	private Set<String> initiallyDisabledJobs = new HashSet<String>();
+	private List<Object> initiallyDisabledJobs = new ArrayList<Object>();
 	
 	public void addAllFlowParameters(Map<String,String> flowParam) {
 		flowParameters.putAll(flowParam);
@@ -175,12 +173,12 @@ public class ExecutionOptions {
 		return queueLevel;
 	}
 	
-	public List<String> getDisabledJobs() {
-		return new ArrayList<String>(initiallyDisabledJobs);
+	public List<Object> getDisabledJobs() {
+		return new ArrayList<Object>(initiallyDisabledJobs);
 	}
 	
-	public void setDisabledJobs(List<String> disabledJobs) {
-		initiallyDisabledJobs = new HashSet<String>(disabledJobs);
+	public void setDisabledJobs(List<Object> disabledJobs) {
+		initiallyDisabledJobs = disabledJobs;
 	}
 	
 	public Map<String,Object> toObject() {
@@ -223,7 +221,7 @@ public class ExecutionOptions {
 		options.concurrentOption = wrapper.getString(CONCURRENT_OPTION, options.concurrentOption);
 		
 		if (wrapper.containsKey(DISABLE)) {
-			options.initiallyDisabledJobs = new HashSet<String>(wrapper.<String>getCollection(DISABLE));
+			options.initiallyDisabledJobs = wrapper.<Object>getList(DISABLE);
 		}
 		
 		if (optionsMap.containsKey(MAIL_CREATOR)) {
diff --git a/src/java/azkaban/executor/ExecutorManager.java b/src/java/azkaban/executor/ExecutorManager.java
index be910d3..408ef2e 100644
--- a/src/java/azkaban/executor/ExecutorManager.java
+++ b/src/java/azkaban/executor/ExecutorManager.java
@@ -403,6 +403,34 @@ public class ExecutorManager extends EventHandler implements ExecutorManagerAdap
 		}
 	}
 	
+	private void applyDisabledJobs(List<Object> disabledJobs, ExecutableFlowBase exflow) {
+		for (Object disabled: disabledJobs) {
+			if (disabled instanceof String) {
+				String nodeName = (String)disabled;
+				ExecutableNode node = exflow.getExecutableNode(nodeName);
+				if (node != null) {
+					node.setStatus(Status.DISABLED);
+				}
+			}
+			else if (disabled instanceof Map) {
+				@SuppressWarnings("unchecked")
+				Map<String,Object> nestedDisabled = (Map<String, Object>)disabled;
+				String nodeName = (String)nestedDisabled.get("id");
+				@SuppressWarnings("unchecked")
+				List<Object> subDisabledJobs = (List<Object>)nestedDisabled.get("children");
+				
+				if (nodeName == null || subDisabledJobs == null) {
+					return;
+				}
+				
+				ExecutableNode node = exflow.getExecutableNode(nodeName);
+				if (node != null && node instanceof ExecutableFlowBase) {
+					applyDisabledJobs(subDisabledJobs, (ExecutableFlowBase)node);
+				}
+			}
+		}
+	}
+	
 	@Override
 	public String submitExecutableFlow(ExecutableFlow exflow, String userId) throws ExecutorManagerException {
 		synchronized(exflow) {
@@ -422,25 +450,7 @@ public class ExecutorManager extends EventHandler implements ExecutorManagerAdap
 			
 			String message = "";
 			if (options.getDisabledJobs() != null) {
-				// Disable jobs
-				for(String disabledId : options.getDisabledJobs()) {
-					String[] splits = disabledId.split(":");
-					ExecutableNode node = exflow;
-					
-					for (String split: splits) {
-						if (node instanceof ExecutableFlowBase) {
-							node = ((ExecutableFlowBase)node).getExecutableNode(split);
-						}
-						else {
-							message = "Cannot disable job " + disabledId + " since flow " + split + " cannot be found. \n";
-						}
-					}
-
-					if (node == null) {
-						throw new ExecutorManagerException("Cannot disable job " + disabledId + ". Cannot find corresponding node.");
-					}
-					node.setStatus(Status.DISABLED);
-				}
+				applyDisabledJobs(options.getDisabledJobs(), exflow);
 			}
 			
 			if (!running.isEmpty()) {
diff --git a/src/java/azkaban/executor/mail/DefaultMailCreator.java b/src/java/azkaban/executor/mail/DefaultMailCreator.java
index 0802cae..831c809 100644
--- a/src/java/azkaban/executor/mail/DefaultMailCreator.java
+++ b/src/java/azkaban/executor/mail/DefaultMailCreator.java
@@ -52,7 +52,7 @@ public class DefaultMailCreator implements MailCreator {
 	public boolean createFirstErrorMessage(ExecutableFlow flow, EmailMessage message, String azkabanName, String clientHostname, String clientPortNumber, String... vars) {
 
 		ExecutionOptions option = flow.getExecutionOptions();
-		List<String> emailList = option.getDisabledJobs();
+		List<String> emailList = option.getFailureEmails();
 		int execId = flow.getExecutionId();
 
 		if (emailList != null && !emailList.isEmpty()) {
diff --git a/src/java/azkaban/jobExecutor/ProcessJob.java b/src/java/azkaban/jobExecutor/ProcessJob.java
index 13272d4..fc9891c 100644
--- a/src/java/azkaban/jobExecutor/ProcessJob.java
+++ b/src/java/azkaban/jobExecutor/ProcessJob.java
@@ -48,20 +48,23 @@ public class ProcessJob extends AbstractProcessJob {
 			resolveProps();
 		}
 		catch (Exception e) {
-			error("Bad property definition! " + e.getMessage());
-			
+			handleError("Bad property definition! " + e.getMessage(), e);
 		}
 		
 		List<String> commands = null;
 		try {
-		commands = getCommandList();
+			commands = getCommandList();
 		}
 		catch (Exception e) {
-			error("Job set up failed " + e.getCause());
+			handleError("Job set up failed " + e.getCause(), e);
 		}
 
 		long startMs = System.currentTimeMillis();
 
+		if (commands == null) {
+			handleError("There are no commands to execute", null);
+		}
+		
 		info(commands.size() + " commands to execute.");
 		File[] propFiles = initPropsFiles();
 		Map<String, String> envVars = getEnvironmentVariables();
@@ -100,7 +103,16 @@ public class ProcessJob extends AbstractProcessJob {
 		generateProperties(propFiles[1]);
 	}
 
-
+	protected void handleError(String errorMsg, Exception e) throws Exception {
+		error(errorMsg);
+		if (e != null) {
+			throw new Exception(errorMsg, e);
+		}
+		else {
+			throw new Exception(errorMsg);
+		}
+	}
+	
 	protected List<String> getCommandList() {
 		List<String> commands = new ArrayList<String>();
 		commands.add(jobProps.getString(COMMAND));
diff --git a/src/java/azkaban/utils/JSONUtils.java b/src/java/azkaban/utils/JSONUtils.java
index e811b82..ad78b6c 100644
--- a/src/java/azkaban/utils/JSONUtils.java
+++ b/src/java/azkaban/utils/JSONUtils.java
@@ -88,6 +88,15 @@ public class JSONUtils {
 		stream.close();
 	}
 	
+	public static Object parseJSONFromStringQuiet(String json) {
+		try {
+			return parseJSONFromString(json);
+		} catch (IOException e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+	
 	public static Object parseJSONFromString(String json) throws IOException {
 		ObjectMapper mapper = new ObjectMapper();
 		JsonFactory factory = new JsonFactory();
diff --git a/src/java/azkaban/webapp/servlet/ExecutorServlet.java b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
index 00ed276..0e155c3 100644
--- a/src/java/azkaban/webapp/servlet/ExecutorServlet.java
+++ b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
@@ -643,125 +643,103 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 		}
 	}
 	
-	private long fillUpdateExecutableFlowInfo(ExecutableFlowBase flow, long lastUpdateTime, HashMap<String, Object> ret) {
-		// Just update the nodes and flow states
-		ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
-		HashMap<String, Map<String,Object>> nodeMap = new HashMap<String, Map<String,Object>>();
-		
-		long updateTime = flow.getUpdateTime();
-		for (ExecutableNode node : flow.getExecutableNodes()) {
-			HashMap<String, Object> nodeObj = null;
-			if (node instanceof ExecutableFlowBase) {
-				nodeObj = new HashMap<String, Object>();
-				long subUpdateTime = fillUpdateExecutableFlowInfo((ExecutableFlowBase)node, lastUpdateTime, nodeObj);
-				updateTime = Math.max(updateTime, subUpdateTime);
-				if (updateTime <= lastUpdateTime) {
-					continue;
-				}
+	private Map<String,Object> getExecutableFlowUpdateInfo(ExecutableNode node, long lastUpdateTime) {
+		HashMap<String, Object> nodeObj = new HashMap<String,Object>();
+		if (node.getUpdateTime() > lastUpdateTime) {
+			nodeObj.put("id", node.getId());
+			nodeObj.put("status", node.getStatus());
+			nodeObj.put("startTime", node.getStartTime());
+			nodeObj.put("endTime", node.getEndTime());
+			nodeObj.put("updateTime", node.getUpdateTime());
+			
+			nodeObj.put("attempt", node.getAttempt());
+			if (node.getAttempt() > 0) {
+				nodeObj.put("pastAttempts", node.getAttemptObjects());
 			}
-			else if (node.getUpdateTime() <= lastUpdateTime){
-				continue;
+		}
+		
+		if (node instanceof ExecutableFlowBase) {
+			ExecutableFlowBase base = (ExecutableFlowBase)node;
+			ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
+			
+			for (ExecutableNode subNode: base.getExecutableNodes()) {
+				Map<String,Object> subNodeObj = getExecutableFlowUpdateInfo(subNode, lastUpdateTime);
+				if (!subNodeObj.isEmpty()) {
+					nodeList.add(subNodeObj);
+				}
 			}
-			else {
-				nodeObj = new HashMap<String, Object>();
-				updateTime = Math.max(updateTime, node.getUpdateTime());
-	
+			
+			if (!nodeList.isEmpty()) {
+				nodeObj.put("flow", base.getFlowId());
+				nodeObj.put("nodes", nodeList);
+				// We do this again, because the above update time may not have been built.
 				nodeObj.put("id", node.getId());
-				nodeObj.put("status", node.getStatus());
-				nodeObj.put("startTime", node.getStartTime());
-				nodeObj.put("endTime", node.getEndTime());
-				nodeObj.put("updateTime", node.getUpdateTime());
-				nodeObj.put("attempt", node.getAttempt());
+			}
+		}
+		
+		return nodeObj;
+	}
 	
-				if (node.getAttempt() > 0) {
-					nodeObj.put("pastAttempts", node.getAttemptObjects());
+	private Map<String,Object> getExecutableNodeInfo(ExecutableNode node) {
+		HashMap<String, Object> nodeObj = new HashMap<String,Object>();
+		nodeObj.put("id", node.getId());
+		nodeObj.put("status", node.getStatus());
+		nodeObj.put("startTime", node.getStartTime());
+		nodeObj.put("endTime", node.getEndTime());
+		nodeObj.put("updateTime", node.getUpdateTime());
+		nodeObj.put("type", node.getType());
+		
+		nodeObj.put("attempt", node.getAttempt());
+		if (node.getAttempt() > 0) {
+			nodeObj.put("pastAttempts", node.getAttemptObjects());
+		}
+		
+		if (node.getInNodes() != null && !node.getInNodes().isEmpty()) {
+			nodeObj.put("in", node.getInNodes());
+		}
+		
+		if (node instanceof ExecutableFlowBase) {
+			ExecutableFlowBase base = (ExecutableFlowBase)node;
+			ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
+			
+			for (ExecutableNode subNode: base.getExecutableNodes()) {
+				Map<String,Object> subNodeObj = getExecutableNodeInfo(subNode);
+				if (!subNodeObj.isEmpty()) {
+					nodeList.add(subNodeObj);
 				}
 			}
 			
-			nodeMap.put(node.getId(), nodeObj);
-			nodeList.add(nodeObj);
+			nodeObj.put("flow", base.getFlowId());
+			nodeObj.put("nodes", nodeList);
+			nodeObj.put("flowId", base.getFlowId());
 		}
-
-		ret.put("nodes", nodeList);
-		ret.put("status", flow.getStatus().toString());
-		ret.put("startTime", flow.getStartTime());
-		ret.put("endTime", flow.getEndTime());
-		ret.put("updateTime", updateTime);
-		return updateTime;
+		
+		return nodeObj;
 	}
 	
-	private void ajaxFetchExecutableFlowUpdate(HttpServletRequest req,
-			HttpServletResponse resp, HashMap<String, Object> ret, User user,
+	private void ajaxFetchExecutableFlowUpdate(
+			HttpServletRequest req,
+			HttpServletResponse resp, 
+			HashMap<String, Object> ret, 
+			User user,
 			ExecutableFlow exFlow) throws ServletException {
 		Long lastUpdateTime = Long.parseLong(getParam(req, "lastUpdateTime"));
 		System.out.println("Fetching " + exFlow.getExecutionId());
 
-		Project project = getProjectAjaxByPermission(ret,
-				exFlow.getProjectId(), user, Type.READ);
+		Project project = getProjectAjaxByPermission(ret, exFlow.getProjectId(), user, Type.READ);
 		if (project == null) {
 			return;
 		}
 		
-		fillUpdateExecutableFlowInfo(exFlow, lastUpdateTime, ret);
+		Map<String, Object> map = getExecutableFlowUpdateInfo(exFlow, lastUpdateTime);
+		ret.putAll(map);
 	}
 
-	private long fillExecutableFlowInfo(ExecutableFlowBase flow, HashMap<String, Object> ret) {
-		long updateTime = flow.getUpdateTime();
-		
-		ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
-		ArrayList<Map<String, Object>> edgeList = new ArrayList<Map<String, Object>>();
-		
-		ArrayList<String> executorQueue = new ArrayList<String>();
-		executorQueue.addAll(flow.getStartNodes());
-
-		for (ExecutableNode node : flow.getExecutableNodes()) {
-			HashMap<String, Object> nodeObj = new HashMap<String, Object>();
-			nodeObj.put("id", node.getId());
-			nodeObj.put("status", node.getStatus());
-			nodeObj.put("startTime", node.getStartTime());
-			nodeObj.put("endTime", node.getEndTime());
-			nodeObj.put("type", node.getType());
-			
-			// Add past attempts
-			if (node.getPastAttemptList() != null) {
-				ArrayList<Object> pastAttempts = new ArrayList<Object>();
-				for (ExecutionAttempt attempt : node.getPastAttemptList()) {
-					pastAttempts.add(attempt.toObject());
-				}
-				nodeObj.put("pastAttempts", pastAttempts);
-			}
-			
-			nodeList.add(nodeObj);
-			
-			// Add edges
-			for (String out : node.getOutNodes()) {
-				HashMap<String, Object> edgeObj = new HashMap<String, Object>();
-				edgeObj.put("from", node.getId());
-				edgeObj.put("target", out);
-				edgeList.add(edgeObj);
-			}
-			
-			// If it's an embedded flow, add the embedded flow info
-			if (node instanceof ExecutableFlowBase) {
-				long subUpdateTime = fillExecutableFlowInfo((ExecutableFlowBase)node, nodeObj);
-				updateTime = Math.max(updateTime, subUpdateTime);
-			}
-			else {
-				nodeObj.put("updateTime", updateTime);
-			}
-		}
-		
-		ret.put("nodes", nodeList);
-		ret.put("edges", edgeList);
-		ret.put("status", flow.getStatus().toString());
-		ret.put("startTime", flow.getStartTime());
-		ret.put("endTime", flow.getEndTime());
-		ret.put("updateTime", updateTime);
-		return updateTime;
-	}
-	
-	private void ajaxFetchExecutableFlow(HttpServletRequest req,
-			HttpServletResponse resp, HashMap<String, Object> ret, User user,
+	private void ajaxFetchExecutableFlow(
+			HttpServletRequest req, 
+			HttpServletResponse resp, 
+			HashMap<String, Object> ret, 
+			User user,
 			ExecutableFlow exFlow) throws ServletException {
 		System.out.println("Fetching " + exFlow.getExecutionId());
 
@@ -771,53 +749,14 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 			return;
 		}
 
-		fillExecutableFlowInfo(exFlow, ret);
 		ret.put("submitTime", exFlow.getSubmitTime());
 		ret.put("submitUser", exFlow.getSubmitUser());
-//		
-//		
-//		ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
-//		ArrayList<Map<String, Object>> edgeList = new ArrayList<Map<String, Object>>();
-//		for (ExecutableNode node : exFlow.getExecutableNodes()) {
-//			HashMap<String, Object> nodeObj = new HashMap<String, Object>();
-//			nodeObj.put("id", node.getId());
-//			nodeObj.put("status", node.getStatus());
-//			nodeObj.put("startTime", node.getStartTime());
-//			nodeObj.put("endTime", node.getEndTime());
-//			nodeObj.put("type", node.getType());
-//			
-//			// Add past attempts
-//			if (node.getPastAttemptList() != null) {
-//				ArrayList<Object> pastAttempts = new ArrayList<Object>();
-//				for (ExecutionAttempt attempt : node.getPastAttemptList()) {
-//					pastAttempts.add(attempt.toObject());
-//				}
-//				nodeObj.put("pastAttempts", pastAttempts);
-//			}
-//
-//			nodeList.add(nodeObj);
-//
-//			// Add edges
-//			for (String out : node.getOutNodes()) {
-//				HashMap<String, Object> edgeObj = new HashMap<String, Object>();
-//				edgeObj.put("from", node.getId());
-//				edgeObj.put("target", out);
-//				edgeList.add(edgeObj);
-//			}
-//			
-//			// If it's an embedded flow, add the embedded flow info
-//			if (node instanceof ExecutableFlowBase) {
-//				
-//			}
-//		}
-//
-//		ret.put("nodes", nodeList);
-//		ret.put("edges", edgeList);
-//		ret.put("status", exFlow.getStatus().toString());
-//		ret.put("startTime", exFlow.getStartTime());
-//		ret.put("endTime", exFlow.getEndTime());
-//		ret.put("submitTime", exFlow.getSubmitTime());
-//		ret.put("submitUser", exFlow.getSubmitUser());
+		ret.put("execid", exFlow.getExecutionId());
+		ret.put("projectId", exFlow.getProjectId());
+		ret.put("project", project.getName());
+		
+		Map<String,Object> flowObj = getExecutableNodeInfo(exFlow);
+		ret.putAll(flowObj);
 	}
 
 	private void ajaxAttemptExecuteFlow(HttpServletRequest req,
diff --git a/src/java/azkaban/webapp/servlet/HttpRequestUtils.java b/src/java/azkaban/webapp/servlet/HttpRequestUtils.java
index 264d3d4..6da9d88 100644
--- a/src/java/azkaban/webapp/servlet/HttpRequestUtils.java
+++ b/src/java/azkaban/webapp/servlet/HttpRequestUtils.java
@@ -19,6 +19,7 @@ package azkaban.webapp.servlet;
 import java.util.Arrays;
 import java.util.Enumeration;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import javax.servlet.ServletException;
@@ -27,6 +28,7 @@ import javax.servlet.http.HttpServletRequest;
 import azkaban.executor.ExecutionOptions;
 import azkaban.executor.ExecutionOptions.FailureAction;
 import azkaban.executor.mail.DefaultMailCreator;
+import azkaban.utils.JSONUtils;
 
 public class HttpRequestUtils {
 	public static ExecutionOptions parseFlowOptions(HttpServletRequest req) throws ServletException {
@@ -101,8 +103,9 @@ public class HttpRequestUtils {
 		if (hasParam(req, "disabled")) {
 			String disabled = getParam(req, "disabled");
 			if (!disabled.isEmpty()) {
-				String[] disabledNodes = disabled.split("\\s*,\\s*");
-				execOptions.setDisabledJobs(Arrays.asList(disabledNodes));
+				@SuppressWarnings("unchecked")
+				List<Object> disabledList = (List<Object>)JSONUtils.parseJSONFromStringQuiet(disabled);
+				execOptions.setDisabledJobs(disabledList);
 			}
 		}
 		return execOptions;
diff --git a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
index 9f907cc..e998b00 100644
--- a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
@@ -553,10 +553,10 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 	private void ajaxFetchFlowGraph(Project project, HashMap<String, Object> ret, HttpServletRequest req) throws ServletException {
 		String flowId = getParam(req, "flow");
 		
-		fillFlowInfo2(project, flowId, ret);
+		fillFlowInfo(project, flowId, ret);
 	}
 	
-	private void fillFlowInfo2(Project project, String flowId, HashMap<String, Object> ret) {
+	private void fillFlowInfo(Project project, String flowId, HashMap<String, Object> ret) {
 		Flow flow = project.getFlow(flowId);
 		
 		ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
@@ -566,9 +566,9 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 			nodeObj.put("type", node.getType());
 			if (node.getEmbeddedFlowId() != null) {
 				nodeObj.put("flowId", node.getEmbeddedFlowId());
-				HashMap<String, Object> embeddedNodeObj = new HashMap<String, Object>();
-				fillFlowInfo2(project, node.getEmbeddedFlowId(), embeddedNodeObj);
-				nodeObj.put("flowData", embeddedNodeObj);
+				//HashMap<String, Object> embeddedNodeObj = new HashMap<String, Object>();
+				fillFlowInfo(project, node.getEmbeddedFlowId(), nodeObj);
+				//nodeObj.put("flowData", embeddedNodeObj);
 			}
 			
 			nodeList.add(nodeObj);
@@ -629,9 +629,7 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 		
 		if (node.getType().equals("flow")) {
 			if (node.getEmbeddedFlowId() != null) {
-				HashMap<String, Object> flowMap = new HashMap<String, Object>();
-				fillFlowInfo2(project, node.getEmbeddedFlowId(), flowMap);
-				ret.put("flowData", flowMap);
+				fillFlowInfo(project, node.getEmbeddedFlowId(), ret);
 			}
 		}
 	}
diff --git a/src/java/azkaban/webapp/servlet/ScheduleServlet.java b/src/java/azkaban/webapp/servlet/ScheduleServlet.java
index c373608..6cc913e 100644
--- a/src/java/azkaban/webapp/servlet/ScheduleServlet.java
+++ b/src/java/azkaban/webapp/servlet/ScheduleServlet.java
@@ -303,21 +303,12 @@ public class ScheduleServlet extends LoginAbstractAzkabanServlet {
 					}
 				}
 			}
-			
-			List<String> disabledJobs;
-			if(flowOptions != null) {
-				disabledJobs = flowOptions.getDisabledJobs() == null ? new ArrayList<String>() : flowOptions.getDisabledJobs();
-			}
-			else {
-				disabledJobs = new ArrayList<String>();
-			}
-				
+
 			List<String> allJobs = new ArrayList<String>();
 			for(Node n : flow.getNodes()) {
-				if(!disabledJobs.contains(n.getId())) {
-					allJobs.add(n.getId());
-				}
+				allJobs.add(n.getId());
 			}
+			
 			ret.put("allJobNames", allJobs);
 		} catch (ServletException e) {
 			ret.put("error", e);
diff --git a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
index b290f48..edc4c5b 100644
--- a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
@@ -20,19 +20,26 @@
 
 #parse("azkaban/webapp/servlet/velocity/style.vm")
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
-
 		<script type="text/javascript" src="${context}/js/moment.min.js"></script>
 		<script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.common.utils.js"></script>
+		
+		 <script type="text/javascript" src="${context}/js/jquery.svg.min.js"></script> 
+ 		<script type="text/javascript" src="${context}/js/jquery.svganim.min.js"></script> 
+		<script type="text/javascript" src="${context}/js/jquery.svgfilter.min.js"></script>
+		
+		<script type="text/javascript" src="${context}/js/svgutils.js"></script> 
 		<script type="text/javascript" src="${context}/js/azkaban.date.utils.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.job.status.utils.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.flow.execute.view.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.flow.job.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.svg.flow.loader.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban.exflow.view.js"></script>
 		<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.exflow.view.js"></script>
+		
 		<script type="text/javascript">
 			var contextURL = "${context}";
 			var currentTime = ${currentTime};
@@ -44,7 +51,6 @@
 			var flowId = "${flowid}";
 			var execId = "${execid}";
 		</script>
-		<link rel="stylesheet" type="text/css" href="${context}/css/bootstrap-datetimepicker.css" />
 
 		<link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui-1.10.1.custom.css" />
 		<link rel="stylesheet" type="text/css" href="${context}/css/azkaban-graph.css" /> 
diff --git a/src/less/flow.less b/src/less/flow.less
index 042f1cd..452a66e 100644
--- a/src/less/flow.less
+++ b/src/less/flow.less
@@ -139,7 +139,7 @@ td {
 			color : #3398cc;
 		}
 	
-		a {
+		> a {
 			clear:both;
 			border-bottom-width: 0;
 		
diff --git a/src/web/css/azkaban-graph.css b/src/web/css/azkaban-graph.css
index 8b1184a..5958152 100644
--- a/src/web/css/azkaban-graph.css
+++ b/src/web/css/azkaban-graph.css
@@ -13,10 +13,12 @@
 
 .node.selected > .nodebox .border {
 	stroke-width: 3;
+	stroke: #39b3d7;
 }
 
 .node.selected > .nodebox .flowborder {
 	stroke-width: 3;
+	fill: #D9EDFF;
 }
 .nodebox > .border:hover {
 	fill-opacity: 0.7;
@@ -72,6 +74,19 @@
 	fill: #FFF;
 }
 
+.KILLED {
+	opacity: 0.5;
+}
+
+.KILLED > g > rect {
+	fill: #d2322d;
+	stroke: #d2322d;
+}
+
+.KILLED > g > text {
+	fill: #FFF;
+}
+
 .FAILED_FINISHING > g > rect {
 	fill: #ed9c28;
 	stroke: #ed9c28;
@@ -91,13 +106,12 @@
 	stroke: #800000;
 }
 
-.SKIPPED > g > rect {
-	fill: #CCC;
-	stroke: #CCC;
+.nodeDisabled {
+	opacity: 0.25;
 }
 
 .SKIPPED > g > rect {
-	fill: #CCC;
+	fill: #DDD;
 	stroke: #CCC;
 }
 
@@ -120,259 +134,3 @@
 	stroke-width: 1.5;
 }
 
-/*
-svg text1 {
-	pointer-events: none;
-}
-
-svg g.nodebox1 {
-	pointer-events: none;
-}
-
-svg .edge {
-	stroke: #BBB;
-	stroke-width: 2;
-}
-
-svg .edge:hover {
-	stroke: #009FC9;
-	stroke-width: 4;
-}
-
-svg .node.disabled {
-	opacity: 0.3;
-}
-
-svg .node .backboard {
-	fill: #FFF;
-	opacity: 0.05;
-}
-
-svg .node:hover {
-	cursor: pointer;
-}
-
-svg .node:hover .backboard {
-	opacity: 0.7;
-}
-
-svg .selected .backboard {
-	opacity: 0.4;
-}
-
-svg .node circle {
-	fill: #888;
-	stroke: #777;
-	stroke-width: 2;
-}
-
-svg .node:hover circle {
-	stroke: #009FC9;
-}
-
-svg .node:hover text {
-	fill: #009FC9;
-}
-
-svg .selected text {
-	fill: #338AB0;
-}
-
-svg .selected circle {
-	stroke: #009FC9;
-	stroke-width: 4;
-}
-
-svg .READY circle {
-	fill: #CCC;
-}
-
-svg .RUNNING circle {
-	fill: #009FC9;
-}
-
-svg .QUEUED circle {
-	opacity: 0.5;
-	fill: #009FC9;
-}
-
-svg .FAILED circle {
-	fill: #CC0000;
-}
-
-svg .KILLED circle {
-	fill: #CC0000;
-}
-
-svg .SUCCEEDED circle {
-	fill: #00CC33;
-}
-
-svg .DISABLED circle {
-	opacity: 0.3;
-}
-
-svg .SKIPPED circle {
-	opacity: 0.3;
-}
-
-svg .selected circle {
-	stroke: #009FC9;
-	stroke-width: 4;
-}
-
-svg .selected .nodebox rect {
-	stroke: #009FC9;
-	stroke-width: 3;
-}
-
-svg .node rect {
-	fill: #CCC;
-	stroke: #CCC;
-	stroke-width: 2;
-}
-
-svg .node .nodebox text {
-	fill: #FFF;
-}
-
-svg .READY .nodebox text {
-	fill: #000;
-}
-
-svg .node:hover rect {
-	stroke: #009FC9;
-}
-
-svg .READY rect {
-	fill: #CCC;
-}
-
-svg .RUNNING rect {
-	fill: #009FC9;
-}
-
-svg .QUEUED rect {
-	opacity: 0.5;
-	fill: #009FC9;
-}
-
-svg .FAILED rect {
-	fill: #CC0000;
-}
-
-svg .KILLED rect {
-	fill: #CC0000;
-}
-
-svg .SUCCEEDED rect {
-	fill: #30ad23;
-}
-
-svg .DISABLED rect {
-	opacity: 0.3;
-}
-
-svg .SKIPPED rect {
-	opacity: 0.3;
-}
-
-svg .nodebox text {
-	fill: #fff;
-}
-
-
-#Used for charts
-svg circle.READY {
-	stroke: #CCC;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.RUNNING {
-	stroke: #009FC9;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.FAILED {
-	stroke: #CC0000;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.KILLED {
-	stroke: #CC0000;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.SUCCEEDED {
-	stroke: #00CC33;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.DISABLED {
-	stroke: #CCC;
-	opacity: 0.3;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-svg circle.SKIPPED {
-	stroke: #CCC;
-	opacity: 0.3;
-	stroke-width: 2px;
-	fill: #FFF;
-}
-
-.flowExtendedView {
-	position: absolute;
-	background-color: rgba(255, 255, 255, 0.95);
-	-moz-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
-	-webkit-box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
-	box-shadow:         1px 1px 5px rgba(0, 0, 0, 0.5);
-
-	min-width: 200px;
-	min-height: 150px;
-}
-
-.dataJobProperties {
-
-}
-
-.flowInfoTitle {
-	padding-top: 8px;
-	padding-left: 8px;
-	padding-bottom: 5px;
-	cursor: pointer;
-}
-
-.flowInfoTitle:hover {
-	background-color: #CCC;
-}
-
-.nodeId {
-	font-size: 16px;
-	font-weight: bold;
-	margin: 20px 20px;
-}
-
-.nodeType {
-	font-style: italic;
-}
-
-.dataContent {
-	margin: 5px;
-}
-
-.dataFlow {
-	width: 100%;
-}
-
-.svgTiny {
-	width: 100%;
-	height: 100%;
-}
-*/
diff --git a/src/web/js/azkaban.exflow.view.js b/src/web/js/azkaban.exflow.view.js
index 4ac616b..e078112 100644
--- a/src/web/js/azkaban.exflow.view.js
+++ b/src/web/js/azkaban.exflow.view.js
@@ -732,20 +732,20 @@ $(function() {
 		model: graphModel
 	});
 	
-  mainSvgGraphView = new azkaban.SvgGraphView({
+	mainSvgGraphView = new azkaban.SvgGraphView({
 		el: $('#svgDiv'), 
 		model: graphModel, 
 		rightClick:	{ 
-			"node": exNodeClickCallback, 
-			"edge": exEdgeClickCallback, 
-			"graph": exGraphClickCallback 
+			"node": nodeClickCallback, 
+			"edge": edgeClickCallback, 
+			"graph": graphClickCallback 
 		}
 	});
 	
   jobsListView = new azkaban.JobListView({
 		el: $('#jobList'), 
 		model: graphModel, 
-		contextMenuCallback: exJobClickCallback
+		contextMenuCallback: jobClickCallback
 	});
 	
   flowLogView = new azkaban.FlowLogView({
@@ -767,52 +767,60 @@ $(function() {
 	var requestData = {"execid": execId, "ajax":"fetchexecflow"};
 	var successHandler = function(data) {
 		console.log("data fetched");
-		graphModel.set({data: data});
-		graphModel.set({disabled: {}});
+		processFlowData(data);
+		graphModel.set({data:data});
 		graphModel.trigger("change:graph");
 		
 		updateTime = Math.max(updateTime, data.submitTime);
 		updateTime = Math.max(updateTime, data.startTime);
 		updateTime = Math.max(updateTime, data.endTime);
-		
-		var nodeMap = {};
-		for (var i = 0; i < data.nodes.length; ++i) {
-			var node = data.nodes[i];
-			nodeMap[node.id] = node;
-			updateTime = Math.max(updateTime, node.startTime);
-			updateTime = Math.max(updateTime, node.endTime);
-		}
-		for (var i = 0; i < data.edges.length; ++i) {
-			var edge = data.edges[i];
-			 
-			if (!nodeMap[edge.target].in) {
-				nodeMap[edge.target].in = {};
-			}
-			var targetInMap = nodeMap[edge.target].in;
-			targetInMap[edge.from] = nodeMap[edge.from];
-			 
-			if (!nodeMap[edge.from].out) {
-				nodeMap[edge.from].out = {};
-			}
-			var sourceOutMap = nodeMap[edge.from].out;
-			sourceOutMap[edge.target] = nodeMap[edge.target];
-		}
-		
-		graphModel.set({nodeMap: nodeMap});
-		if (window.location.hash) {
-			var hash = window.location.hash;
-			if (hash == "#jobslist") {
-				flowTabView.handleJobslistLinkClick();
-			}
-			else if (hash == "#log") {
-				flowTabView.handleLogLinkClick();
-			}
-		}
-		else {
-			flowTabView.handleGraphLinkClick();
-		}
-		updaterFunction();
-		logUpdaterFunction();
+//		
+//		graphModel.set({data: data});
+//		graphModel.set({disabled: {}});
+//		graphModel.trigger("change:graph");
+//		
+//		updateTime = Math.max(updateTime, data.submitTime);
+//		updateTime = Math.max(updateTime, data.startTime);
+//		updateTime = Math.max(updateTime, data.endTime);
+//		
+//		var nodeMap = {};
+//		for (var i = 0; i < data.nodes.length; ++i) {
+//			var node = data.nodes[i];
+//			nodeMap[node.id] = node;
+//			updateTime = Math.max(updateTime, node.startTime);
+//			updateTime = Math.max(updateTime, node.endTime);
+//		}
+//		for (var i = 0; i < data.edges.length; ++i) {
+//			var edge = data.edges[i];
+//			 
+//			if (!nodeMap[edge.target].in) {
+//				nodeMap[edge.target].in = {};
+//			}
+//			var targetInMap = nodeMap[edge.target].in;
+//			targetInMap[edge.from] = nodeMap[edge.from];
+//			 
+//			if (!nodeMap[edge.from].out) {
+//				nodeMap[edge.from].out = {};
+//			}
+//			var sourceOutMap = nodeMap[edge.from].out;
+//			sourceOutMap[edge.target] = nodeMap[edge.target];
+//		}
+//		
+//		graphModel.set({nodeMap: nodeMap});
+//		if (window.location.hash) {
+//			var hash = window.location.hash;
+//			if (hash == "#jobslist") {
+//				flowTabView.handleJobslistLinkClick();
+//			}
+//			else if (hash == "#log") {
+//				flowTabView.handleLogLinkClick();
+//			}
+//		}
+//		else {
+//			flowTabView.handleGraphLinkClick();
+//		}
+//		updaterFunction();
+//		logUpdaterFunction();
 	};
 	ajaxCall(requestURL, requestData, successHandler);
 });
diff --git a/src/web/js/azkaban.flow.execute.view.js b/src/web/js/azkaban.flow.execute.view.js
index c09af4c..909917d 100644
--- a/src/web/js/azkaban.flow.execute.view.js
+++ b/src/web/js/azkaban.flow.execute.view.js
@@ -16,26 +16,6 @@
 
 $.namespace('azkaban');
 
-function recurseAllAncestors(nodes, disabledMap, id, disable) {
-	var node = nodes[id];
-	if (node.in) {
-		for (var key in node.in) {
-			disabledMap[key] = disable;
-			recurseAllAncestors(nodes, disabledMap, key, disable);
-		}
-	}
-}
-
-function recurseAllDescendents(nodes, disabledMap, id, disable) {
-	var node = nodes[id];
-	if (node.out) {
-		for (var key in node.out) {
-			disabledMap[key] = disable;
-			recurseAllDescendents(nodes, disabledMap, key, disable);
-		}
-	}
-}
-
 var flowExecuteDialogView;
 azkaban.FlowExecuteDialogView = Backbone.View.extend({
 	events: {
@@ -90,20 +70,15 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 			}
 		}
 		
-		var disabled = "";
-		var disabledMap = this.model.get('disabled');
-		for (var dis in disabledMap) {
-			if (disabledMap[dis]) {
-				disabled += dis + ",";
-			}
-		}
+		var data = this.model.get("data");
+		var disabledList = gatherDisabledNodes(data);
 		
 		var executingData = {
 			projectId: projectId,
 			project: this.projectName,
 			ajax: "executeFlow",
 			flow: this.flowId,
-			disabled: disabled,
+			disabled: JSON.stringify(disabledList),
 			failureEmailsOverride:failureEmailsOverride,
 			successEmailsOverride:successEmailsOverride,
 			failureAction: failureAction,
@@ -128,7 +103,6 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 		
 		return executingData;
 	},
-	
 	changeFlowInfo: function() {
 		var successEmails = this.model.get("successEmails");
 		var failureEmails = this.model.get("failureEmails");
@@ -183,28 +157,7 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 		if (queueLevel) {
 			$('#queueLevel').val(queueLevel);
 		}
-		
-		if (nodeStatus) {
-			var nodeMap = this.model.get("nodeMap");
-			var disabled = {};
-			for (var key in nodeStatus) {
-				var status = nodeStatus[key];
-				
-				var node = nodeMap[key];
-				if (node) {
-					node.status = status;
-					if (node.status == "DISABLED" || node.status == "SKIPPED") {
-						node.status = "READY";
-						disabled[node.id] = true;
-					}
-					if (node.status == "SUCCEEDED" || node.status=="RUNNING") {
-						disabled[node.id] = true;
-					}
-				}
-			}
-			this.model.set({"disabled":disabled});
-		}
-		
+
 		if (flowParams) {
 			for (var key in flowParams) {
 				editTableView.handleAddRow({
@@ -253,15 +206,12 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 		var disabled = this.model.get("disabled");
 		
 		// Disable all, then re-enable those you want.
-		for (var key in nodes) {
-			disabled[key] = true;
-		}
+		disableAll();
 		
 		var jobNode = nodes[jobId];
-		disabled[jobId] = false;
 		
 		if (withDep) {
-			recurseAllAncestors(nodes, disabled, jobId, false);
+			recurseAllAncestors(jobNode, false);
 		}
 
 		this.showExecutionOptionPanel();
@@ -293,7 +243,7 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 		var requestURL = contextURL + "/manager";
 		
 		var graphModel = executableGraphModel;
-		//fetchFlow(this.model, projectName, flowId, true);
+		// fetchFlow(this.model, projectName, flowId, true);
 		var requestData = {
 				"project": projectName, 
 				"ajax": "fetchflowgraph", 
@@ -302,6 +252,7 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 		var successHandler = function(data) {
 			console.log("data fetched");
 			processFlowData(data);
+			disableFinishedJobs(data);
 			graphModel.set({data:data});
 			
 			executingSvgGraphView = new azkaban.SvgGraphView({
@@ -309,9 +260,9 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 				model: graphModel,
 				render: true,
 				rightClick: { 
-					"node": nodeClickCallback,
-					"edge": edgeClickCallback, 
-					"graph": graphClickCallback 
+					"node": exNodeClickCallback,
+					"edge": exEdgeClickCallback, 
+					"graph": exGraphClickCallback 
 				}
 			});
 		};
@@ -350,16 +301,16 @@ azkaban.EditTableView = Backbone.View.extend({
 	
 		var tr = document.createElement("tr");
 		var tdName = document.createElement("td");
-    $(tdName).addClass('property-key');
+		$(tdName).addClass('property-key');
 		var tdValue = document.createElement("td");
 		
 		var remove = document.createElement("div");
-    $(remove).addClass("pull-right").addClass('remove-btn');
-    var removeBtn = document.createElement("button");
-    $(removeBtn).attr('type', 'button');
-    $(removeBtn).addClass('btn').addClass('btn-xs').addClass('btn-danger');
-    $(removeBtn).text('Delete');
-    $(remove).append(removeBtn);
+		$(remove).addClass("pull-right").addClass('remove-btn');
+		var removeBtn = document.createElement("button");
+		$(removeBtn).attr('type', 'button');
+		$(removeBtn).addClass('btn').addClass('btn-xs').addClass('btn-danger');
+		$(removeBtn).text('Delete');
+		$(remove).append(removeBtn);
 
 		var nameData = document.createElement("span");
 		$(nameData).addClass("spanValue");
@@ -372,7 +323,7 @@ azkaban.EditTableView = Backbone.View.extend({
 		$(tdName).addClass("editable");
 		
 		$(tdValue).append(valueData);
-    $(tdValue).append(remove);
+		$(tdValue).append(remove);
 		$(tdValue).addClass("editable").addClass('value');
 		
 		$(tr).addClass("editRow");
@@ -391,7 +342,7 @@ azkaban.EditTableView = Backbone.View.extend({
 					
 		var input = document.createElement("input");
 		$(input).attr("type", "text");
-    $(input).addClass('form-control').addClass('input-sm');
+		$(input).addClass('form-control').addClass('input-sm');
 		$(input).css("width", "100%");
 		$(input).val(text);
 		$(curTarget).addClass("editing");
@@ -428,13 +379,13 @@ azkaban.EditTableView = Backbone.View.extend({
 		$(valueData).text(text);
 
 		if ($(parent).hasClass("value")) {
-      var remove = document.createElement("div");
-      $(remove).addClass("pull-right").addClass('remove-btn');
-      var removeBtn = document.createElement("button");
-      $(removeBtn).attr('type', 'button');
-      $(removeBtn).addClass('btn').addClass('btn-xs').addClass('btn-danger');
-      $(removeBtn).text('Delete');
-      $(remove).append(removeBtn);
+			var remove = document.createElement("div");
+			$(remove).addClass("pull-right").addClass('remove-btn');
+			var removeBtn = document.createElement("button");
+			$(removeBtn).attr('type', 'button');
+			$(removeBtn).addClass('btn').addClass('btn-xs').addClass('btn-danger');
+			$(removeBtn).text('Delete');
+			$(remove).append(removeBtn);
 			$(parent).append(remove);
 		}
 		
@@ -488,7 +439,7 @@ azkaban.SideMenuDialogView = Backbone.View.extend({
 var handleJobMenuClick = function(action, el, pos) {
 	var jobid = el[0].jobid;
 	
-	var requesgURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowName + "&job=" + jobid;
+	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowName + "&job=" + jobid;
 	if (action == "open") {
 		window.location.href = requestURL;
 	}
@@ -500,106 +451,185 @@ var handleJobMenuClick = function(action, el, pos) {
 var executableGraphModel;
 azkaban.GraphModel = Backbone.Model.extend({});
 
+/**
+ * Disable jobs that need to be disabled
+ */
+var disableFinishedJobs = function(data) {
+	for (var i=0; i < data.nodes.length; ++i) {
+		var node = data.nodes[i];
+		node.status = status;
+		if (node.status == "DISABLED" || node.status == "SKIPPED") {
+			node.status = "READY";
+			node.disabled = true;
+		}
+		else if (node.status == "SUCCEEDED" || node.status=="RUNNING") {
+			node.disabled = true;
+		}
+		else {
+			node.disabled = false;
+			if (node.flowData) {
+				disableFinishedJobs(node.flowData);
+			}
+		}
+	}
+}
+
+/**
+ * Enable all jobs. Recurse
+ */
 var enableAll = function() {
-	disabled = {};
-	executableGraphModel.set({disabled: disabled});
+	recurseTree(executableGraphModel.get("data"), false, false);
 	executableGraphModel.trigger("change:disabled");
 }
 
 var disableAll = function() {
-	var disabled = executableGraphModel.get("disabled");
-
-	var nodes = executableGraphModel.get("nodes");
-	for (var key in nodes) {
-		disabled[key] = true;
-	}
-
-	executableGraphModel.set({disabled: disabled});
+	recurseTree(executableGraphModel.get("data"), true, false);
 	executableGraphModel.trigger("change:disabled");
 }
 
-var touchNode = function(jobid, disable) {
-	var disabled = executableGraphModel.get("disabled");
+var recurseTree = function(data, disabled, recurse) {
+	for (var i=0; i < data.nodes.length; ++i) {
+		var node = data.nodes[i];
+		node.disabled = disabled;
+		
+		if (node.flowData && recurse) {
+			recurseTree(node.flowData, disabled);
+		}
+	}
+}
 
-	disabled[jobid] = disable;
-	executableGraphModel.set({disabled: disabled});
+var touchNode = function(node, disable) {
+	node.disabled = disable;
 	executableGraphModel.trigger("change:disabled");
 }
 
-var touchParents = function(jobid, disable) {
-	var disabled = executableGraphModel.get("disabled");
-	var nodes = executableGraphModel.get("nodes");
-	var inNodes = nodes[jobid].inNodes;
+var touchParents = function(node, disable) {
+	var inNodes = node.inNodes;
 
 	if (inNodes) {
 		for (var key in inNodes) {
-			disabled[key] = disable;
+			inNodes[key].disabled = disable;
 		}
 	}
-	
-	executableGraphModel.set({disabled: disabled});
+
 	executableGraphModel.trigger("change:disabled");
 }
 
-var touchChildren = function(jobid, disable) {
-	var disabledMap = executableGraphModel.get("disabled");
-	var nodes = executableGraphModel.get("nodes");
-	var outNodes = nodes[jobid].outNodes;
+var touchChildren = function(node, disable) {
+	var outNodes = node.outNodes;
 
 	if (outNodes) {
 		for (var key in outNodes) {
-			disabledMap[key] = disable;
+			outNodes[key].disabled = disable;
 		}
 	}
 	
-	executableGraphModel.set({disabled: disabledMap});
 	executableGraphModel.trigger("change:disabled");
 }
 
-var touchAncestors = function(jobid, disable) {
-	var disabled = executableGraphModel.get("disabled");
-	var nodes = executableGraphModel.get("nodes");
-	
-	recurseAllAncestors(nodes, disabled, jobid, disable);
+var touchAncestors = function(node, disable) {
+	recurseAllAncestors(node, disable);
 	
-	executableGraphModel.set({disabled: disabled});
 	executableGraphModel.trigger("change:disabled");
 }
 
 var touchDescendents = function(jobid, disable) {
-	var disabled = executableGraphModel.get("disabled");
-	var nodes = executableGraphModel.get("nodes");
-	
-	recurseAllDescendents(nodes, disabled, jobid, disable);
+	recurseAllDescendents(node, disable);
 	
-	executableGraphModel.set({disabled: disabled});
 	executableGraphModel.trigger("change:disabled");
 }
 
-var exNodeClickCallback = function(event) {
+var gatherDisabledNodes = function(data) {
+	var nodes = data.nodes;
+	var disabled = [];
+	
+	for (var i = 0; i < nodes.length; ++i) {
+		var node = nodes[i];
+		if (node.disabled) {
+			disabled.push(node.id);
+		}
+		else {
+			if (node.flowData) {
+				var array = gatherDisabledNodes(node.flowData);
+				if (array && array.length > 0) {
+					disabled.push({id: node.id, children: array});
+				}
+			}
+		}
+	}
+	
+	return disabled;
+}
+
+function recurseAllAncestors(node, disable) {
+	var inNodes = node.inNodes;
+	if (inNodes) {
+		for (var key in inNodes) {
+			inNodes[key].disabled = disable;
+			recurseAllAncestors(inNodes[key], disable);
+		}
+	}
+}
+
+function recurseAllDescendents(node, disable) {
+	var outNodes = node.outNodes;
+	if (outNodes) {
+		for (var key in outNodes) {
+			outNodes[key].disabled = disable;
+			recurseAllDescendents(outNodes[key], disable);
+		}
+	}
+}
+
+var exNodeClickCallback = function(event, model, node) {
 	console.log("Node clicked callback");
-	var jobId = event.currentTarget.jobid;
+	var jobId = node.id;
 	var flowId = executableGraphModel.get("flowId");
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
+	var type = node.type;
+	
+	var menu;
+	if (type == "flow") {
+		var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + node.flowId;
+		if (node.expanded) {
+			menu = [
+				{title: "Collapse Flow...", callback: function() {model.trigger("collapseFlow", node);}},
+				{title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}}
+			];
+	
+		}
+		else {
+			menu = [
+				{title: "Expand Flow...", callback: function() {model.trigger("expandFlow", node);}},
+				{title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}}
+			];
+		}
+	}
+	else {
+		var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
+		menu = [
+				{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+			];
+	}
 
-	var menu = [
-		{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+	$.merge(menu, [
 		{break: 1},
-		{title: "Enable", callback: function() {touchNode(jobId, false);}, submenu: [
-			{title: "Parents", callback: function(){touchParents(jobId, false);}},
-			{title: "Ancestors", callback: function(){touchAncestors(jobId, false);}},
-			{title: "Children", callback: function(){touchChildren(jobId, false);}},
-			{title: "Descendents", callback: function(){touchDescendents(jobId, false);}},
+		{title: "Enable", callback: function() {touchNode(node, false);}, submenu: [
+			{title: "Parents", callback: function(){touchParents(node, false);}},
+			{title: "Ancestors", callback: function(){touchAncestors(node, false);}},
+			{title: "Children", callback: function(){touchChildren(node, false);}},
+			{title: "Descendents", callback: function(){touchDescendents(node, false);}},
 			{title: "Enable All", callback: function(){enableAll();}}
 		]},
-		{title: "Disable", callback: function() {touchNode(jobId, true)}, submenu: [
-			{title: "Parents", callback: function(){touchParents(jobId, true);}},
-			{title: "Ancestors", callback: function(){touchAncestors(jobId, true);}},
-			{title: "Children", callback: function(){touchChildren(jobId, true);}},
-			{title: "Descendents", callback: function(){touchDescendents(jobId, true);}},
+		{title: "Disable", callback: function() {touchNode(node, true)}, submenu: [
+			{title: "Parents", callback: function(){touchParents(node, true);}},
+			{title: "Ancestors", callback: function(){touchAncestors(node, true);}},
+			{title: "Children", callback: function(){touchChildren(node, true);}},
+			{title: "Descendents", callback: function(){touchDescendents(node, true);}},
 			{title: "Disable All", callback: function(){disableAll();}}
-		]}
-	];
+		]},
+		{title: "Center Job", callback: function() {model.trigger("centerNode", node);}}
+	]);
+
 
 	contextMenuView.show(event, menu);
 }
diff --git a/src/web/js/azkaban.flow.job.view.js b/src/web/js/azkaban.flow.job.view.js
index 4cf6c13..c987a90 100644
--- a/src/web/js/azkaban.flow.job.view.js
+++ b/src/web/js/azkaban.flow.job.view.js
@@ -33,6 +33,7 @@ azkaban.JobListView = Backbone.View.extend({
 		this.list = $(this.el).find("#joblist");
 		this.contextMenu = settings.contextMenuCallback;
 		this.listNodes = {};
+		
 	},
 	filterJobs: function(self) {
 		var filter = this.filterInput.val();
@@ -110,12 +111,14 @@ azkaban.JobListView = Backbone.View.extend({
 			var node = data.nodes[i];
 			if (node.status) {
 				var liElement = node.listElement;
-				$(liElement).removeClass(statusList.join(' '));
-				$(liElement).addClass(node.status);
+				var child = $(liElement).children("a");
+				$(child).removeClass(statusList.join(' '));
+				$(child).addClass(node.status);
+				$(child).attr("title", node.status + " (" + node.type + ")");
 			}
 			
-			if (node.flowData) {
-				this.changeStatuses(node.flowData);
+			if (node.type == "flow") {
+				this.changeStatuses(node);
 			}
 		}
 	},
@@ -126,7 +129,9 @@ azkaban.JobListView = Backbone.View.extend({
 		this.renderTree(this.list, data);
 //		
 //		this.assignInitialStatus(self);
-//		this.handleDisabledChange(self);
+		this.handleDisabledChange(self);
+		this.changeStatuses(data);
+		$("li.listElement > a").tooltip({delay: {show: 500, hide: 100}, placement: 'top'});
 	},
 	renderTree : function(el, data, prefix) {
 		var nodes = data.nodes;
@@ -174,14 +179,14 @@ azkaban.JobListView = Backbone.View.extend({
 			$(li).append(a);
 			$(ul).append(li);
 			
-			if (nodeArray[i].flowData) {
+			if (nodeArray[i].type == "flow") {
 				// Add the up down
 				var expandDiv = document.createElement("div");
 				$(expandDiv).addClass("expandarrow glyphicon glyphicon-chevron-down");
 				$(a).append(expandDiv);
 				
 				// Create subtree
-				var subul = this.renderTree(li, nodeArray[i].flowData, listNodeName + ":");
+				var subul = this.renderTree(li, nodeArray[i], listNodeName + ":");
 				$(subul).hide();
 			}
 		}
@@ -245,18 +250,21 @@ azkaban.JobListView = Backbone.View.extend({
 		else {
 			this.model.set({"selected": node});
 		}
-
 	},
 	handleDisabledChange: function(evt) {
-		var disabledMap = this.model.get("disabled");
-		var nodes = this.model.get("nodes");
-		
-		for(var id in nodes) {
-			if (disabledMap[id]) {
-				$(this.listNodes[id]).addClass("nodedisabled");
+		this.changeDisabled(this.model.get('data'));
+	},
+	changeDisabled: function(data) {
+		for (var i =0; i < data.nodes; ++i) {
+			var node = data.nodes[i];
+			if (node.disabled = true) {
+				removeClass(node.listElement, "nodedisabled");
+				if (node.type=='flow') {
+					this.changeDisabled(node);
+				}
 			}
 			else {
-				$(this.listNodes[id]).removeClass("nodedisabled");
+				addClass(node.listElement, "nodedisabled");
 			}
 		}
 	},
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index a6fd36e..d0c7e7f 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -16,19 +16,6 @@
 
 $.namespace('azkaban');
 
-var statusStringMap = {
-	"FAILED": "Failed",
-	"SUCCEEDED": "Success",
-	"FAILED_FINISHING": "Running w/Failure",
-	"RUNNING": "Running",
-	"WAITING": "Waiting",
-	"KILLED": "Killed",
-	"DISABLED": "Disabled",
-	"READY": "Ready",
-	"UNKNOWN": "Unknown",
-	"QUEUED": "Queued"
-};
-
 var handleJobMenuClick = function(action, el, pos) {
 	var jobid = el[0].jobid;
 	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + 
@@ -302,28 +289,29 @@ azkaban.SummaryView = Backbone.View.extend({
 		this.model.bind('render', this.render, this);
 		
 		this.fetchDetails();
-    this.fetchSchedule();
+		this.fetchSchedule();
 		this.fetchLastRun();
 		this.model.trigger('render');
 	},
 
-  fetchDetails: function() {
-    var requestURL = contextURL + "/manager";
-    var requestData = {
-      'ajax': 'fetchflowdetails',
-      'project': projectName,
-      'flow': flowId
-    };
+	fetchDetails: function() {
+		var requestURL = contextURL + "/manager";
+		var requestData = {
+			'ajax': 'fetchflowdetails',
+			'project': projectName,
+			'flow': flowId
+		};
+		
 		var model = this.model;
-    var successHandler = function(data) {
-      console.log(data);
-      model.set({
-        'jobTypes': data.jobTypes
-      });
-      model.trigger('render');
-    };
-    $.get(requestURL, requestData, successHandler, 'json');
-  },
+		var successHandler = function(data) {
+			console.log(data);
+			model.set({
+				'jobTypes': data.jobTypes
+			});
+			model.trigger('render');
+		};
+		$.get(requestURL, requestData, successHandler, 'json');
+	},
 
 	fetchSchedule: function() {
 		var requestURL = contextURL + "/schedule"
@@ -334,8 +322,8 @@ azkaban.SummaryView = Backbone.View.extend({
 		};
 		var model = this.model;
 		var successHandler = function(data) {
-      model.set({'schedule': data.schedule});
-      model.trigger('render');
+			model.set({'schedule': data.schedule});
+			model.trigger('render');
 		};
 		$.get(requestURL, requestData, successHandler, 'json');
 	},
@@ -349,9 +337,9 @@ azkaban.SummaryView = Backbone.View.extend({
 
 	render: function(evt) {
 		var data = {
-      projectName: projectName,
+			projectName: projectName,
 			flowName: flowId,
-      jobTypes: this.model.get('jobTypes'),
+			jobTypes: this.model.get('jobTypes'),
 			general: this.model.get('general'),
 			schedule: this.model.get('schedule'),
 			lastRun: this.model.get('lastRun')
diff --git a/src/web/js/azkaban.project.view.js b/src/web/js/azkaban.project.view.js
index ef01a0e..ccfeeb5 100644
--- a/src/web/js/azkaban.project.view.js
+++ b/src/web/js/azkaban.project.view.js
@@ -81,8 +81,8 @@ azkaban.FlowTableView = Backbone.View.extend({
 			var level = job.level;
 			var nodeId = flowId + "-" + name;
 	
-      var li = document.createElement('li');
-      $(li).addClass("list-group-item");
+			var li = document.createElement('li');
+			$(li).addClass("list-group-item");
 			$(li).attr("id", nodeId);
 			li.flowId = flowId;
 			li.dependents = job.dependents;
diff --git a/src/web/js/azkaban.svg.flow.loader.js b/src/web/js/azkaban.svg.flow.loader.js
index a3cbc71..24098fa 100644
--- a/src/web/js/azkaban.svg.flow.loader.js
+++ b/src/web/js/azkaban.svg.flow.loader.js
@@ -1,3 +1,16 @@
+var statusStringMap = {
+	"FAILED": "Failed",
+	"SUCCEEDED": "Success",
+	"FAILED_FINISHING": "Running w/Failure",
+	"RUNNING": "Running",
+	"WAITING": "Waiting",
+	"KILLED": "Killed",
+	"DISABLED": "Disabled",
+	"READY": "Ready",
+	"UNKNOWN": "Unknown",
+	"QUEUED": "Queued"
+};
+
 var extendedViewPanels = {};
 var extendedDataModels = {};
 var openJobDisplayCallback = function(nodeId, flowId, evt) {
@@ -81,10 +94,8 @@ var processFlowData = function(data) {
 	for (var key in nodes) {
 		var node = nodes[key];
 		node.parent = data;
-		if (node.type == "flow" && node.flowData) {
-			processFlowData(node.flowData);
-			// Weird cycle. Evaluate whether we can instead unwrap these things.
-			node.flowData.node = node;
+		if (node.type == "flow") {
+			processFlowData(node);
 		}
 	}
 	
@@ -92,7 +103,6 @@ var processFlowData = function(data) {
 	console.log("data fetched");
 	data.nodeMap = nodes;
 	data.edges = edges;
-	data.disabled = {};
 }
 
 var closeAllSubDisplays = function() {
diff --git a/src/web/js/azkaban.svg.graph.view.js b/src/web/js/azkaban.svg.graph.view.js
index cf6d2c4..968de08 100644
--- a/src/web/js/azkaban.svg.graph.view.js
+++ b/src/web/js/azkaban.svg.graph.view.js
@@ -19,7 +19,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
 	events: {
 		
 	},
-  initialize: function(settings) {
+	initialize: function(settings) {
 		this.model.bind('change:selected', this.changeSelected, this);
 		this.model.bind('centerNode', this.centerNode, this);
 		this.model.bind('change:graph', this.render, this);
@@ -136,10 +136,6 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		bounds.maxY = bounds.maxY ? bounds.maxY + margin : margin;
 		
 		this.assignInitialStatus(this, data);
-		
-		if (data.disabled && data.disabled.length > 0) {
-			this.handleDisabledChange(self);
-		}
 
 		if (self.rightClick) {
 			if (self.rightClick.node) {
@@ -162,24 +158,31 @@ azkaban.SvgGraphView = Backbone.View.extend({
 					return false;
 				});
 			}
-
 		};
+		
+		$(".node").each( 
+				function(d,i){
+					$(this).tooltip({container:"body", delay: {show: 500, hide: 100}});
+				});
 
 		return bounds;
 	},
-	
-  handleDisabledChange: function(evt) {
-		var disabledMap = this.model.get("disabled");
-
-		for(var id in this.nodes) {
-			 var g = this.gNodes[id];
-			if (disabledMap[id]) {
-				this.nodes[id].disabled = true;
-				addClass(g, "disabled");
+	handleDisabledChange: function(evt) {
+		this.changeDisabled(this.model.get('data'));
+	},
+	changeDisabled: function(data) {
+		for (var i =0; i < data.nodes.length; ++i) {
+			var node = data.nodes[i];
+			if (node.disabled) {
+				addClass(node.gNode, "nodeDisabled");
 			}
 			else {
-				this.nodes[id].disabled = false;
-				removeClass(g, "disabled");
+				if (node.gNode) {
+					removeClass(node.gNode, "nodeDisabled");
+				}
+				if (node.type=='flow') {
+					this.changeDisabled(node);
+				}
 			}
 		}
 	},
@@ -188,9 +191,13 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			var updateNode = data.nodes[i];
 			var g = updateNode.gNode;
 			var initialStatus = updateNode.status ? updateNode.status : "READY";
-
+			
 			addClass(g, initialStatus);
-			$(g).attr("title", initialStatus);
+			$(g).attr("title", updateNode.status + " (" + updateNode.type + ")");
+			
+			if (updateNode.disabled) {
+				addClass(g, "nodeDisabled");
+			}
 		}
 	},
 	changeSelected: function(self) {
@@ -215,28 +222,32 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		}
 	},
   propagateExpansion: function(node) {
-		if (node.parent) {
-			if (node.parent.node) {
-				this.propagateExpansion(node.parent.node);
-				this.expandFlow(node.parent.node);
-			}
+		if (node.parent.type) {
+			this.propagateExpansion(node.parent);
+			this.expandFlow(node.parent);
 		}
 	},
   handleStatusUpdate: function(evt) {
 		var updateData = this.model.get("update");
-		if (updateData.nodes) {
-			for (var i = 0; i < updateData.nodes.length; ++i) {
-				var updateNode = updateData.nodes[i];
+		this.updateStatusChanges(updatedData);
+	},
+	updateStatusChanges: function(changedData) {
+		// Assumes all changes have been applied.
+		if (changedData.nodes) {
+			var nodeMap = previousData.nodeMap;
+			for (var i = 0; i < changedData.nodes.length; ++i) {
+				var node = changedData.nodes[i];
+				var nodeToUpdate = nodeMap[updateNode.id];
 				
-				var g = this.gNodes[updateNode.id];
+				var g = nodeToUpdate.gNode;
 				this.handleRemoveAllStatus(g);
+				addClass(g, nodeToUpdate.status);
+				$(g).attr("title", updateNode.status + " (" + updateNode.type + ")");
 				
-				addClass(g, updateNode.status);
-				$(g).attr("title", updateNode.status);
+				this.updateStatusChanges(node);
 			}
 		}
 	},
-	
   handleRemoveAllStatus: function(gNode) {
 		for (var j = 0; j < statusList.length; ++j) {
 			var status = statusList[j];
@@ -244,13 +255,6 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		}
 	},
 	
-  clickGraph: function(self) {
-		console.log("click");
-		if (self.currentTarget.data) {
-			this.model.set({"selected": self.currentTarget.data});
-		}
-	},
-	
   handleRightClick: function(self) {
 		if (this.rightClick) {
 			var callbacks = this.rightClick;
@@ -323,7 +327,6 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		var innerG = gnode.innerG;
 		var borderRect = innerG.borderRect;
 		var labelG = innerG.labelG;
-		var flowData = node.flowData;
 		
 		var bbox;
 		if (!innerG.expandedFlow) {
@@ -331,7 +334,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			var hmargin = 10;
 		
 			var expandedFlow = svg.group(innerG, "", {class: "expandedGraph"});
-			this.renderGraph(flowData, expandedFlow);
+			this.renderGraph(node, expandedFlow);
 			innerG.expandedFlow = expandedFlow;
 			removeClass(innerG, "collapsed");
 			addClass(innerG, "expanded");
@@ -366,7 +369,6 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		var innerG = gnode.innerG;
 		var borderRect = innerG.borderRect;
 		var labelG = innerG.labelG;
-		var flowData = node.flowData;
 
 		removeClass(innerG, "expanded");
 		addClass(innerG, "collapsed");
@@ -395,15 +397,11 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		var parent = node.parent;
 		if (parent) {
 			layoutGraph(parent.nodes, parent.edges, 10);
-			if (parent.node) {
-				this.relayoutFlow(parent.node);
-			}
+			this.relayoutFlow(parent);
+			// Move all points again.
+			this.moveNodeEdges(parent.nodes, parent.edges);
+			this.animateExpandedFlowNode(node, 250);
 		}
-		
-		// Move all points again.
-		this.moveNodeEdges(parent.nodes, parent.edges);
-		this.animateExpandedFlowNode(node, 250);
-		
 	},
 	moveNodeEdges: function(nodes, edges) {
 		var svg = this.svg;
@@ -563,7 +561,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		var labelG = innerG.labelG;
 		var expandedFlow = innerG.expandedFlow;
 		
-		var bound = this.calculateBounds(node.flowData.nodes);
+		var bound = this.calculateBounds(node.nodes);
 		
 		node.height = bound.height + topmargin + bottommargin;
 		node.width = bound.width + hmargin*2;
@@ -601,9 +599,9 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		this.panZoom({x: x, y: y, width: node.width, height: node.height});
 	},
 	globalNodePosition: function(gNode) {
-		if (node.parent.node) {
+		if (node.parent) {
 		
-			var parentPos = this.globalNodePosition(node.parent.node);
+			var parentPos = this.globalNodePosition(node.parent);
 			return {x: parentPos.x + node.x, y: parentPos.y + node.y};
 		}
 		else {
diff --git a/unit/java/azkaban/test/executor/ExecutableFlowTest.java b/unit/java/azkaban/test/executor/ExecutableFlowTest.java
index 9031518..9f2a3c2 100644
--- a/unit/java/azkaban/test/executor/ExecutableFlowTest.java
+++ b/unit/java/azkaban/test/executor/ExecutableFlowTest.java
@@ -108,7 +108,7 @@ public class ExecutableFlowTest {
 		
 		ExecutionOptions options = new ExecutionOptions();
 		options.setConcurrentOption("blah");
-		options.setDisabledJobs(Arrays.asList(new String[] {"bee", null, "boo"}));
+		options.setDisabledJobs(Arrays.asList(new Object[] {"bee", null, "boo"}));
 		options.setFailureAction(FailureAction.CANCEL_ALL);
 		options.setFailureEmails(Arrays.asList(new String[] {"doo", null, "daa"}));
 		options.setSuccessEmails(Arrays.asList(new String[] {"dee", null, "dae"}));
@@ -287,7 +287,7 @@ public class ExecutableFlowTest {
 		Assert.assertEquals(optionsA.isFailureEmailsOverridden(), optionsB.isFailureEmailsOverridden());
 		Assert.assertEquals(optionsA.isSuccessEmailsOverridden(), optionsB.isSuccessEmailsOverridden());
 		
-		testEquals(optionsA.getDisabledJobs(), optionsB.getDisabledJobs());
+		testDisabledEquals(optionsA.getDisabledJobs(), optionsB.getDisabledJobs());
 		testEquals(optionsA.getSuccessEmails(), optionsB.getSuccessEmails());
 		testEquals(optionsA.getFailureEmails(), optionsB.getFailureEmails());
 		testEquals(optionsA.getFlowParameters(), optionsB.getFlowParameters());
@@ -329,11 +329,43 @@ public class ExecutableFlowTest {
 		while(iterA.hasNext()) {
 			String aStr = iterA.next();
 			String bStr = iterB.next();
-			
 			Assert.assertEquals(aStr, bStr);
 		}
 	}
 	
+	@SuppressWarnings("unchecked")
+	public static void testDisabledEquals(List<Object> a, List<Object> b) {
+		if (a == b) {
+			return;
+		}
+		
+		if (a == null || b == null) {
+			Assert.fail();
+		}
+		
+		Assert.assertEquals(a.size(), b.size());
+		
+		Iterator<Object> iterA = a.iterator();
+		Iterator<Object> iterB = b.iterator();
+		
+		while(iterA.hasNext()) {
+			Object aStr = iterA.next();
+			Object bStr = iterB.next();
+			
+			if (aStr instanceof Map && bStr instanceof Map) {
+				Map<String, Object> aMap = (Map<String, Object>)aStr;
+				Map<String, Object> bMap = (Map<String, Object>)bStr;
+				
+				Assert.assertEquals((String)aMap.get("id"), (String)bMap.get("id"));
+				testDisabledEquals((List<Object>)aMap.get("children"), (List<Object>)bMap.get("children"));
+			}
+			else {
+				Assert.assertEquals(aStr, bStr);
+			}
+		}
+	}
+	
+	
 	public static void testEquals(Map<String, String> a, Map<String, String> b) {
 		if (a == b) {
 			return;
diff --git a/unit/java/azkaban/test/trigger/ExecuteFlowActionTest.java b/unit/java/azkaban/test/trigger/ExecuteFlowActionTest.java
index 905ca3d..4c49dab 100644
--- a/unit/java/azkaban/test/trigger/ExecuteFlowActionTest.java
+++ b/unit/java/azkaban/test/trigger/ExecuteFlowActionTest.java
@@ -21,7 +21,7 @@ public class ExecuteFlowActionTest {
 		loader.init(new Props());
 		
 		ExecutionOptions options = new ExecutionOptions();
-		List<String> disabledJobs = new ArrayList<String>();
+		List<Object> disabledJobs = new ArrayList<Object>();
 		options.setDisabledJobs(disabledJobs);
 		
 		ExecuteFlowAction executeFlowAction = new ExecuteFlowAction("ExecuteFlowAction", 1, "testproject", "testflow", "azkaban", options, null);