azkaban-developers

Details

diff --git a/src/java/azkaban/execapp/JobRunner.java b/src/java/azkaban/execapp/JobRunner.java
index 912844e..99bd8ab 100644
--- a/src/java/azkaban/execapp/JobRunner.java
+++ b/src/java/azkaban/execapp/JobRunner.java
@@ -361,9 +361,9 @@ public class JobRunner extends EventHandler implements Runnable {
 				);
 				Arrays.sort(files, Collections.reverseOrder());
 				
-				loader.uploadLogFile(executionId, this.jobId, node.getAttempt(), files);
+				loader.uploadLogFile(executionId, this.node.getNestedId(), node.getAttempt(), files);
 			} catch (ExecutorManagerException e) {
-				flowLogger.error("Error writing out logs for job " + this.jobId, e);
+				flowLogger.error("Error writing out logs for job " + this.node.getNestedId(), e);
 			}
 		}
 		else {
diff --git a/src/java/azkaban/webapp/servlet/ExecutorServlet.java b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
index 0e155c3..808ba7b 100644
--- a/src/java/azkaban/webapp/servlet/ExecutorServlet.java
+++ b/src/java/azkaban/webapp/servlet/ExecutorServlet.java
@@ -115,9 +115,6 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 				else if (ajaxName.equals("cancelFlow")) {
 					ajaxCancelFlow(req, resp, ret, session.getUser(), exFlow);
 				}
-				else if (ajaxName.equals("restartFlow")) {
-					ajaxRestartFlow(req, resp, ret, session.getUser(), exFlow);
-				}
 				else if (ajaxName.equals("pauseFlow")) {
 					ajaxPauseFlow(req, resp, ret, session.getUser(), exFlow);
 				}
@@ -609,13 +606,6 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 			ret.put("execIds", refs);
 		}
 	}
-	
-	private void ajaxRestartFlow(HttpServletRequest req, HttpServletResponse resp, HashMap<String, Object> ret, User user, ExecutableFlow exFlow) throws ServletException{
-		Project project = getProjectAjaxByPermission(ret, exFlow.getProjectId(), user, Type.EXECUTE);
-		if (project == null) {
-			return;
-		}
-	}
 
 	private void ajaxPauseFlow(HttpServletRequest req, HttpServletResponse resp, HashMap<String, Object> ret, User user, ExecutableFlow exFlow) throws ServletException{
 		Project project = getProjectAjaxByPermission(ret, exFlow.getProjectId(), user, Type.EXECUTE);
@@ -645,19 +635,6 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 	
 	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());
-			}
-		}
-		
 		if (node instanceof ExecutableFlowBase) {
 			ExecutableFlowBase base = (ExecutableFlowBase)node;
 			ArrayList<Map<String, Object>> nodeList = new ArrayList<Map<String, Object>>();
@@ -672,8 +649,19 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 			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());
+			}
+		}
+		
+		if (node.getUpdateTime() > lastUpdateTime || !nodeObj.isEmpty()) {
+			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());
 			}
 		}
 		
@@ -688,6 +676,7 @@ public class ExecutorServlet extends LoginAbstractAzkabanServlet {
 		nodeObj.put("endTime", node.getEndTime());
 		nodeObj.put("updateTime", node.getUpdateTime());
 		nodeObj.put("type", node.getType());
+		nodeObj.put("nestedId", node.getNestedId());
 		
 		nodeObj.put("attempt", node.getAttempt());
 		if (node.getAttempt() > 0) {
diff --git a/src/less/flow.less b/src/less/flow.less
index 452a66e..5408f07 100644
--- a/src/less/flow.less
+++ b/src/less/flow.less
@@ -63,6 +63,14 @@
 }
 
 td {
+	> .listExpand {
+		width: 16px;
+		height: 16px;
+		float:right;
+		margin-top: 5px;
+		font-size: 8pt;
+	}
+
   .status {
     -moz-border-radius: 2px;
     border-radius: 2px;
@@ -194,6 +202,7 @@ td {
 				float: right;
 				width: 16px;
 				height: 16px;
+				font-size: 8pt;
 			}
 			
 			.filterHighlight {
diff --git a/src/web/css/azkaban-graph.css b/src/web/css/azkaban-graph.css
index 5958152..6d1112b 100644
--- a/src/web/css/azkaban-graph.css
+++ b/src/web/css/azkaban-graph.css
@@ -74,13 +74,9 @@
 	fill: #FFF;
 }
 
-.KILLED {
-	opacity: 0.5;
-}
-
 .KILLED > g > rect {
-	fill: #d2322d;
-	stroke: #d2322d;
+	fill: #FF9999;
+	stroke: #FF9999;
 }
 
 .KILLED > g > text {
@@ -97,13 +93,13 @@
 }
 
 .DISABLED > g > rect {
-	fill: #800000;
-	stroke: #800000;
+	fill: #DDD;
+	stroke: #CCC;
 }
 
 .DISABLED > g > rect {
-	fill: #800000;
-	stroke: #800000;
+	fill: #DDD;
+	stroke: #CCC;
 }
 
 .nodeDisabled {
diff --git a/src/web/js/azkaban.exflow.view.js b/src/web/js/azkaban.exflow.view.js
index e078112..168de67 100644
--- a/src/web/js/azkaban.exflow.view.js
+++ b/src/web/js/azkaban.exflow.view.js
@@ -218,14 +218,15 @@ azkaban.FlowTabView = Backbone.View.extend({
 	},
 	
 	handleRestartClick: function(evt) {
-    console.log("handleRestartClick");
+		console.log("handleRestartClick");
 		var data = graphModel.get("data");
-		var nodes = data.nodes;
+		
 		var executingData = {
 			project: projectName,
 			ajax: "executeFlow",
 			flow: flowId,
-			execid: execId
+			execid: execId,
+			exgraph: data
 		};
 		flowExecuteDialogView.show(executingData);
 	},
@@ -318,73 +319,77 @@ azkaban.ExecutionListView = Backbone.View.extend({
 	},
 	
 	updateJobRow: function(nodes) {
+		if (!nodes) {
+			return;
+		}
+		
 		var executingBody = $("#executableBody");
 		nodes.sort(function(a,b) { return a.startTime - b.startTime; });
 		
 		for (var i = 0; i < nodes.length; ++i) {
 			var node = nodes[i];
 			if (node.startTime < 0) {
-        continue;
-      }
-      var nodeId = node.id.replace(".", "\\\\.");
-      var row = document.getElementById(nodeId + "-row");
-      if (!row) {
-        this.addNodeRow(node);
-      }
-      
-      var div = $("#" + nodeId + "-status-div");
-      div.text(statusStringMap[node.status]);
-      $(div).attr("class", "status " + node.status);
-      
-      var startdate = new Date(node.startTime);
-      $("#" + nodeId + "-start").text(getDateFormat(startdate));
-      
-      var endTime = node.endTime;
-      if (node.endTime == -1) {
-        $("#" + nodeId + "-end").text("-");
-        endTime = node.startTime + 1;
-      }
-      else {
-        var enddate = new Date(node.endTime);
-        $("#" + nodeId + "-end").text(getDateFormat(enddate));
-      }
-      
-      var progressBar = $("#" + nodeId + "-progressbar");
-      if (!progressBar.hasClass(node.status)) {
-        for (var j = 0; j < statusList.length; ++j) {
-          var status = statusList[j];
-          progressBar.removeClass(status);
-        }
-        progressBar.addClass(node.status);
-      }
-      
-      // Create past attempts
-      if (node.pastAttempts) {
-        for (var a = 0; a < node.pastAttempts.length; ++a) {
-          var attemptBarId = nodeId + "-progressbar-" + a;
-          var attempt = node.pastAttempts[a];
-          if ($("#" + attemptBarId).length == 0) {
-            var attemptBox = document.createElement("div");
-            $(attemptBox).attr("id", attemptBarId);
-            $(attemptBox).addClass("flow-progress-bar");
-            $(attemptBox).addClass("attempt");
-            $(attemptBox).addClass(attempt.status);
-            $(attemptBox).css("float","left");
-            $(attemptBox).bind("contextmenu", attemptRightClick);
-            $(progressBar).before(attemptBox);
-            attemptBox.job = nodeId;
-            attemptBox.attempt = a;
-          }
-        }
-      }
-      
-      if (node.endTime == -1) {
-        //$("#" + node.id + "-elapse").text("0 sec");
-        $("#" + nodeId + "-elapse").text(getDuration(node.startTime, (new Date()).getTime()));					
-      }
-      else {
-        $("#" + nodeId + "-elapse").text(getDuration(node.startTime, node.endTime));
+				continue;
+		}
+		var nodeId = node.id.replace(".", "\\\\.");
+		var row = document.getElementById(nodeId + "-row");
+		if (!row) {
+			this.addNodeRow(node);
+		}
+		
+		var div = $("#" + nodeId + "-status-div");
+		div.text(statusStringMap[node.status]);
+		$(div).attr("class", "status " + node.status);
+
+		var startdate = new Date(node.startTime);
+		$("#" + nodeId + "-start").text(getDateFormat(startdate));
+  
+		var endTime = node.endTime;
+		if (node.endTime == -1) {
+			$("#" + nodeId + "-end").text("-");
+			endTime = node.startTime + 1;
+		}
+		else {
+			var enddate = new Date(node.endTime);
+			$("#" + nodeId + "-end").text(getDateFormat(enddate));
+		}
+  
+		var progressBar = $("#" + nodeId + "-progressbar");
+		if (!progressBar.hasClass(node.status)) {
+			for (var j = 0; j < statusList.length; ++j) {
+				var status = statusList[j];
+				progressBar.removeClass(status);
+		}
+		progressBar.addClass(node.status);
+	}
+  
+  // Create past attempts
+  if (node.pastAttempts) {
+    for (var a = 0; a < node.pastAttempts.length; ++a) {
+      var attemptBarId = nodeId + "-progressbar-" + a;
+      var attempt = node.pastAttempts[a];
+      if ($("#" + attemptBarId).length == 0) {
+        var attemptBox = document.createElement("div");
+        $(attemptBox).attr("id", attemptBarId);
+        $(attemptBox).addClass("flow-progress-bar");
+        $(attemptBox).addClass("attempt");
+        $(attemptBox).addClass(attempt.status);
+        $(attemptBox).css("float","left");
+        $(attemptBox).bind("contextmenu", attemptRightClick);
+        $(progressBar).before(attemptBox);
+        attemptBox.job = nodeId;
+        attemptBox.attempt = a;
       }
+    }
+  }
+  
+  if (node.endTime == -1) {
+    //$("#" + node.id + "-elapse").text("0 sec");
+    $("#" + nodeId + "-elapse").text(getDuration(node.startTime, (new Date()).getTime()));					
+  }
+  else {
+    $("#" + nodeId + "-elapse").text(getDuration(node.startTime, node.endTime));
+  }
 		}
 	},
 	
@@ -452,8 +457,29 @@ azkaban.ExecutionListView = Backbone.View.extend({
 			elem.attr("title", "attempt:" + elem.attempt + "	start:" + getHourMinSec(new Date(node.startTime)) + "	end:" + getHourMinSec(new Date(node.endTime)));
 		}
 	},
-	
+	toggleExpandFlow: function(flow) {
+		console.log("Toggle Expand");
+		var tr = flow.progressbar;
+		var expandIcon = $(tr).find("> td > .listExpand");
+		if (tr.expanded) {
+			tr.expanded = false;
+			$(expandIcon).removeClass("glyphicon-chevron-up");
+			$(expandIcon).addClass("glyphicon-chevron-down");
+		}
+		else {
+			tr.expanded = true;
+			$(expandIcon).addClass("glyphicon-chevron-up");
+			$(expandIcon).removeClass("glyphicon-chevron-down");
+		}
+	},
+	expandFlow: function(flow) {
+		for (var i = 0; i < flow.nodes.length; ++i) {
+			var node = flow.nodes[i];
+			///@TODO Expand.
+		}
+	},
 	addNodeRow: function(node) {
+		var self = this;
 		var executingBody = $("#executableBody");
 		var tr = document.createElement("tr");
 		var tdName = document.createElement("td");
@@ -463,6 +489,8 @@ azkaban.ExecutionListView = Backbone.View.extend({
 		var tdElapse = document.createElement("td");
 		var tdStatus = document.createElement("td");
 		var tdDetails = document.createElement("td");
+		node.progressbar = tr;
+		tr.node = node;
 		
 		$(tr).append(tdName);
 		$(tr).append(tdTimeline);
@@ -495,24 +523,35 @@ azkaban.ExecutionListView = Backbone.View.extend({
 		$(a).attr("href", requestURL);
 		$(a).text(node.id);
 		$(tdName).append(a);
+		if (node.type=="flow") {
+			var expandIcon = document.createElement("div");
+			$(expandIcon).addClass("listExpand");
+			$(tdName).append(expandIcon);
+			$(expandIcon).addClass("expandarrow glyphicon glyphicon-chevron-down");
+			$(expandIcon).click(function(evt) {
+				var parent = $(evt.currentTarget).parents("tr")[0];
+				self.toggleExpandFlow(parent.node);
+			});
+		}
 
 		var status = document.createElement("div");
 		$(status).addClass("status");
 		$(status).attr("id", node.id + "-status-div");
 		tdStatus.appendChild(status);
 
-		var logURL = contextURL + "/executor?execid=" + execId + "&job=" + node.id;
+		var logURL = contextURL + "/executor?execid=" + execId + "&job=" + node.nestedId;
 		if (node.attempt) {
 			logURL += "&attempt=" + node.attempt;
 		}
 
-		var a = document.createElement("a");
-		$(a).attr("href", logURL);
-		$(a).attr("id", node.id + "-log-link");
-		$(a).text("Details");
-		$(tdDetails).addClass("details");
-		$(tdDetails).append(a);
-		
+		if (node.type != 'flow' && node.status != 'SKIPPED') {
+			var a = document.createElement("a");
+			$(a).attr("href", logURL);
+			$(a).attr("id", node.id + "-log-link");
+			$(a).text("Details");
+			$(tdDetails).addClass("details");
+			$(tdDetails).append(a);
+		}
 		executingBody.append(tr);
 	}
 });
@@ -577,6 +616,7 @@ var updateStatus = function() {
 	var oldData = graphModel.get("data");
 	var nodeMap = graphModel.get("nodeMap");
 	
+	var updateTime = oldData.updateTime ? oldData.updateTime : 0;
 	var requestData = {
 		"execid": execId, 
 		"ajax": "fetchexecflowupdate", 
@@ -586,23 +626,8 @@ var updateStatus = function() {
 	var successHandler = function(data) {
 		console.log("data updated");
 		updateTime = data.updateTime;
-		oldData.submitTime = data.submitTime;
-		oldData.startTime = data.startTime;
-		oldData.endTime = data.endTime;
-		oldData.status = data.status;
 		
-		for (var i = 0; i < data.nodes.length; ++i) {
-			var node = data.nodes[i];
-			var oldNode = nodeMap[node.id];
-			oldNode.startTime = node.startTime;
-			oldNode.updateTime = node.updateTime;
-			oldNode.endTime = node.endTime;
-			oldNode.status = node.status;
-			oldNode.attempt = node.attempt;
-			if (oldNode.attempt > 0) {
-				oldNode.pastAttempts = node.pastAttempts;
-			}
-		}
+		updateGraph(oldData, data);
 
 		graphModel.set({"update": data});
 		graphModel.trigger("change:update");
@@ -610,6 +635,23 @@ var updateStatus = function() {
 	ajaxCall(requestURL, requestData, successHandler);
 }
 
+var updateGraph = function(data, update) {
+	var nodeMap = data.nodeMap;
+	data.startTime = update.startTime;
+	data.endTime = update.endTime;
+	data.updateTime = update.updateTime;
+	data.status = update.status;
+	
+	if (update.nodes) {
+		for (var i = 0; i < update.nodes.length; ++i) {
+			var newNode = update.nodes[i];
+			var oldNode = nodeMap[newNode.id];
+			
+			updateGraph(oldNode, newNode);
+		}
+	}
+}
+
 var updateTime = -1;
 var updaterFunction = function() {
 	var oldData = graphModel.get("data");
@@ -742,18 +784,18 @@ $(function() {
 		}
 	});
 	
-  jobsListView = new azkaban.JobListView({
+	jobsListView = new azkaban.JobListView({
 		el: $('#jobList'), 
 		model: graphModel, 
 		contextMenuCallback: jobClickCallback
 	});
 	
-  flowLogView = new azkaban.FlowLogView({
+	flowLogView = new azkaban.FlowLogView({
 		el: $('#flowLogView'), 
 		model: logModel
 	});
 	
-  statusView = new azkaban.StatusView({
+	statusView = new azkaban.StatusView({
 		el: $('#flow-status'), 
 		model: graphModel
 	});
@@ -819,8 +861,8 @@ $(function() {
 //		else {
 //			flowTabView.handleGraphLinkClick();
 //		}
-//		updaterFunction();
-//		logUpdaterFunction();
+		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 909917d..700b4eb 100644
--- a/src/web/js/azkaban.flow.execute.view.js
+++ b/src/web/js/azkaban.flow.execute.view.js
@@ -175,11 +175,12 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 		
 		// ExecId is optional
 		var execId = data.execid;
-	
+		var exgraph = data.exgraph;
+		
 		var loadedId = executableGraphModel.get("flowId");
-		this.loadGraph(projectName, flowId);
+		this.loadGraph(projectName, flowId, exgraph);
 		this.loadFlowInfo(projectName, flowId, execId);
-
+		
 		this.projectName = projectName;
 		this.flowId = flowId;
 		if (jobId) {
@@ -238,7 +239,7 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 		fetchFlowInfo(this.model, projectName, flowId, execId);
 	},
 	
-	loadGraph: function(projectName, flowId) {
+	loadGraph: function(projectName, flowId, exgraph) {
 		console.log("Loading flow " + flowId);
 		var requestURL = contextURL + "/manager";
 		
@@ -249,26 +250,47 @@ azkaban.FlowExecuteDialogView = Backbone.View.extend({
 				"ajax": "fetchflowgraph", 
 				"flow": flowId
 			};
+		var self = this;
 		var successHandler = function(data) {
 			console.log("data fetched");
 			processFlowData(data);
-			disableFinishedJobs(data);
 			graphModel.set({data:data});
 			
+			if (exgraph) {
+				self.assignInitialStatus(data, exgraph);
+			}
+			
+			// Auto disable jobs that are finished.
+			disableFinishedJobs(data);
 			executingSvgGraphView = new azkaban.SvgGraphView({
 				el: $('#flow-executing-graph'), 
 				model: graphModel,
 				render: true,
 				rightClick: { 
-					"node": exNodeClickCallback,
-					"edge": exEdgeClickCallback, 
-					"graph": exGraphClickCallback 
-				}
+					"node": expanelNodeClickCallback,
+					"edge": expanelEdgeClickCallback, 
+					"graph": expanelGraphClickCallback 
+				},
+				tooltipcontainer: "#svg-div-custom"
 			});
 		};
 		$.get(requestURL, requestData, successHandler, "json");
 	},
-	
+	assignInitialStatus: function(data, statusData) {
+		// Copies statuses over from the previous execution if it exists.
+		var statusNodeMap = statusData.nodeMap;
+		var nodes = data.nodes;
+		for(var i=0; i<nodes.length; ++i) {
+			var node = nodes[i];
+			var statusNode = statusNodeMap[node.id];
+			if (statusNode) {
+				node.status = statusNode.status;
+				if (node.type == "flow" && statusNode.type == "flow") {
+					this.assignInitialStatus(node, statusNode);
+				}
+			}
+		}
+	},
 	handleExecuteFlow: function(evt) {
 		console.log("click schedule button.");
 		var executeURL = contextURL + "/executor";
@@ -457,7 +479,7 @@ azkaban.GraphModel = Backbone.Model.extend({});
 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;
@@ -465,6 +487,10 @@ var disableFinishedJobs = function(data) {
 		else if (node.status == "SUCCEEDED" || node.status=="RUNNING") {
 			node.disabled = true;
 		}
+		else if (node.status == "KILLED") {
+			node.disabled = false;
+			node.status="READY";
+		}
 		else {
 			node.disabled = false;
 			if (node.flowData) {
@@ -581,7 +607,7 @@ function recurseAllDescendents(node, disable) {
 	}
 }
 
-var exNodeClickCallback = function(event, model, node) {
+var expanelNodeClickCallback = function(event, model, node) {
 	console.log("Node clicked callback");
 	var jobId = node.id;
 	var flowId = executableGraphModel.get("flowId");
@@ -634,11 +660,11 @@ var exNodeClickCallback = function(event, model, node) {
 	contextMenuView.show(event, menu);
 }
 
-var exEdgeClickCallback = function(event) {
+var expanelEdgeClickCallback = function(event) {
 	console.log("Edge clicked callback");
 }
 
-var exGraphClickCallback = function(event) {
+var expanelGraphClickCallback = function(event) {
 	console.log("Graph clicked callback");
 	var flowId = executableGraphModel.get("flowId");
 	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
diff --git a/src/web/js/azkaban.svg.flow.loader.js b/src/web/js/azkaban.svg.flow.loader.js
index 24098fa..e629758 100644
--- a/src/web/js/azkaban.svg.flow.loader.js
+++ b/src/web/js/azkaban.svg.flow.loader.js
@@ -8,7 +8,8 @@ var statusStringMap = {
 	"DISABLED": "Disabled",
 	"READY": "Ready",
 	"UNKNOWN": "Unknown",
-	"QUEUED": "Queued"
+	"QUEUED": "Queued",
+	"SKIPPED": "Skipped"
 };
 
 var extendedViewPanels = {};
diff --git a/src/web/js/azkaban.svg.graph.view.js b/src/web/js/azkaban.svg.graph.view.js
index 968de08..be35b0d 100644
--- a/src/web/js/azkaban.svg.graph.view.js
+++ b/src/web/js/azkaban.svg.graph.view.js
@@ -69,6 +69,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			});
 		}
 
+		this.tooltipcontainer = settings.tooltipcontainer ? settings.tooltipcontainer : "body";
 		if (settings.render) {
 			this.render();
 		}
@@ -161,9 +162,8 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		};
 		
 		$(".node").each( 
-				function(d,i){
-					$(this).tooltip({container:"body", delay: {show: 500, hide: 100}});
-				});
+				function(d,i){$(this).tooltip({container:self.tooltipcontainer, delay: {show: 500, hide: 100}});
+		});
 
 		return bounds;
 	},
@@ -174,11 +174,15 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		for (var i =0; i < data.nodes.length; ++i) {
 			var node = data.nodes[i];
 			if (node.disabled) {
-				addClass(node.gNode, "nodeDisabled");
+				if (node.gNode) {
+					addClass(node.gNode, "nodeDisabled");
+					$(node.gNode).attr("title", "DISABLED (" + node.type + ")").tooltip('fixTitle');
+				}
 			}
 			else {
 				if (node.gNode) {
 					removeClass(node.gNode, "nodeDisabled");
+					$(node.gNode).attr("title", node.status + " (" + node.type + ")").tooltip('fixTitle');
 				}
 				if (node.type=='flow') {
 					this.changeDisabled(node);
@@ -193,11 +197,13 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			var initialStatus = updateNode.status ? updateNode.status : "READY";
 			
 			addClass(g, initialStatus);
-			$(g).attr("title", updateNode.status + " (" + updateNode.type + ")");
+			var title = updateNode.status + " (" + updateNode.type + ")";
 			
 			if (updateNode.disabled) {
 				addClass(g, "nodeDisabled");
+				title = "DISABLED (" + updateNode.type + ")";
 			}
+			$(g).attr("title", title);
 		}
 	},
 	changeSelected: function(self) {
@@ -221,30 +227,41 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			}
 		}
 	},
-  propagateExpansion: function(node) {
+	propagateExpansion: function(node) {
 		if (node.parent.type) {
 			this.propagateExpansion(node.parent);
 			this.expandFlow(node.parent);
 		}
 	},
-  handleStatusUpdate: function(evt) {
+	handleStatusUpdate: function(evt) {
 		var updateData = this.model.get("update");
-		this.updateStatusChanges(updatedData);
+		var data = this.model.get("data");
+		this.updateStatusChanges(updateData, data);
 	},
-	updateStatusChanges: function(changedData) {
+	updateStatusChanges: function(updateData, data) {
 		// 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];
+		if (updateData.nodes) {
+			var nodeMap = data.nodeMap;
+			for (var i = 0; i < updateData.nodes.length; ++i) {
+				var node = updateData.nodes[i];
+				var nodeToUpdate = nodeMap[node.id];
 				
 				var g = nodeToUpdate.gNode;
-				this.handleRemoveAllStatus(g);
-				addClass(g, nodeToUpdate.status);
-				$(g).attr("title", updateNode.status + " (" + updateNode.type + ")");
-				
-				this.updateStatusChanges(node);
+				if (g) {
+					this.handleRemoveAllStatus(g);
+					addClass(g, nodeToUpdate.status);
+					
+					var title = nodeToUpdate.status + " (" + nodeToUpdate.type + ")";
+					if (nodeToUpdate.disabled) {
+						addClass(g, "nodeDisabled");
+						title = "DISABLED (" + nodeToUpdate.type + ")";
+					}
+					$(g).attr("title", title).tooltip('fixTitle');
+					
+					if (node.nodes) {
+						this.updateStatusChanges(node, nodeToUpdate);
+					}
+				}
 			}
 		}
 	},