azkaban-uncached

Display of Embedded flows working for regular display. Onto

7/30/2013 12:41:49 AM

Details

diff --git a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
index ea5b901..4abde57 100644
--- a/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
+++ b/src/java/azkaban/webapp/servlet/ProjectManagerServlet.java
@@ -64,6 +64,7 @@ import azkaban.user.User;
 import azkaban.utils.JSONUtils;
 import azkaban.utils.Pair;
 import azkaban.utils.Props;
+import azkaban.utils.PropsUtils;
 import azkaban.utils.Utils;
 import azkaban.webapp.AzkabanWebServer;
 import azkaban.webapp.session.Session;
@@ -204,6 +205,11 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 				ajaxFetchFlowGraph(project, ret, req);
 			}
 		}
+		else if (ajaxName.equals("fetchflownodedata")) {
+			if (handleAjaxPermission(project, user, Type.READ, ret)) {
+				ajaxFetchFlowNodeData(project, ret, req);
+			}
+		}
 		else if (ajaxName.equals("fetchprojectflows")) {
 			if (handleAjaxPermission(project, user, Type.READ, ret)) {
 				ajaxFetchProjectFlows(project, ret, req);
@@ -413,7 +419,6 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 	}
 	
 	private void ajaxFetchJobInfo(Project project, HashMap<String, Object> ret, HttpServletRequest req) throws ServletException {
-
 		String flowName = getParam(req, "flowName");
 		String jobName = getParam(req, "jobName");
 		
@@ -505,6 +510,11 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 	
 	private void ajaxFetchFlowGraph(Project project, HashMap<String, Object> ret, HttpServletRequest req) throws ServletException {
 		String flowId = getParam(req, "flow");
+		
+		fillFlowInfo(project, flowId, ret);
+	}
+	
+	private void fillFlowInfo(Project project, String flowId, HashMap<String, Object> ret) {
 		Flow flow = project.getFlow(flowId);
 		
 		//Collections.sort(flowNodes, NODE_LEVEL_COMPARATOR);
@@ -539,6 +549,47 @@ public class ProjectManagerServlet extends LoginAbstractAzkabanServlet {
 		ret.put("edges", edgeList);
 	}
 	
+	private void ajaxFetchFlowNodeData(Project project, HashMap<String, Object> ret, HttpServletRequest req) throws ServletException {
+		String flowId = getParam(req, "flow");
+		Flow flow = project.getFlow(flowId);
+		
+		String nodeId = getParam(req, "node");
+		Node node = flow.getNode(nodeId);
+		
+		if (node == null) {
+			ret.put("error", "Job " + nodeId + " doesn't exist.");
+			return;
+		}
+		
+		ret.put("id", nodeId);
+		ret.put("flow", flowId);
+		ret.put("type", node.getType());
+		
+		Props props;
+		try {
+			props = projectManager.getProperties(project, node.getJobSource());
+		} catch (ProjectManagerException e) {
+			ret.put("error", "Failed to upload job override property for " + nodeId);
+			return;
+		}
+		
+		if (props == null) {
+			ret.put("error", "Properties for " + nodeId + " isn't found.");
+			return;
+		}
+		
+		Map<String,String> properties = PropsUtils.toStringMap(props, true);
+		ret.put("props", properties);
+		
+		if (node.getType().equals("flow")) {
+			if (node.getEmbeddedFlowId() != null) {
+				HashMap<String, Object> flowMap = new HashMap<String, Object>();
+				fillFlowInfo(project, node.getEmbeddedFlowId(), flowMap);
+				ret.put("flowData", flowMap);
+			}
+		}
+	}
+	
 	private void ajaxFetchFlow(Project project, HashMap<String, Object> ret, HttpServletRequest req, HttpServletResponse resp) throws ServletException {
 		String flowId = getParam(req, "flow");
 		Flow flow = project.getFlow(flowId);
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm b/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
index aab0e61..32619e0 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowexecutionpanel.vm
@@ -1,7 +1,6 @@
 <script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
 <script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
 <script type="text/javascript" src="${context}/js/azkaban.context.menu.js"></script>
-<script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
 <script type="text/javascript" src="${context}/js/azkaban.flow.execute.view.js"></script>
 <link rel="stylesheet" type="text/css" href="${context}/css/azkaban-graph.css" /> 
 
@@ -147,6 +146,3 @@
 #parse( "azkaban/webapp/servlet/velocity/schedulepanel.vm" )
 #end
 
-<div id="contextMenu">
-	
-</div>
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowextendedpanel.vm b/src/java/azkaban/webapp/servlet/velocity/flowextendedpanel.vm
new file mode 100644
index 0000000..557d09c
--- /dev/null
+++ b/src/java/azkaban/webapp/servlet/velocity/flowextendedpanel.vm
@@ -0,0 +1,22 @@
+<div id="flowInfoBase" class="flowExtendedView" style="display:none">
+	<div class="flowExtendedViewHeader">
+		<h3 class="flowInfoTitle"><span class="nodeId"></span><span class="nodeType"></span></h3>
+		<a title="Close" class="modal-close closeInfoPanel">x</a>
+	</div>
+	<div class="dataContent">
+		<div class="dataFlow">
+		</div>
+		<div class="dataJobProperties">
+			<table class="dataPropertiesTable">
+				<thead class="dataPropertiesHead">
+					<tr>
+						<th>Name</th>
+						<th>Value</th>
+					</tr>
+				</thead>
+				<tbody class="dataPropertiesBody">
+				</tbody>
+			</table>
+		</div>
+	</div>
+</div>
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index 1bec383..82101cb 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -30,9 +30,13 @@
 		<script type="text/javascript" src="${context}/js/azkaban.ajax.utils.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.nav.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.layout.js"></script>
+		<script type="text/javascript" src="${context}/js/svgNavigate.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.svg.graph.helper.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.svg.graph.view.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban.flow.extended.view.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban.flow.job.view.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};
@@ -147,6 +151,7 @@
 #end
 
 		</div>
+		#parse( "azkaban/webapp/servlet/velocity/flowextendedpanel.vm" )
 		<div id="contextMenu">
 		</div>
 		#parse( "azkaban/webapp/servlet/velocity/messagedialog.vm" )
diff --git a/src/java/azkaban/webapp/servlet/velocity/projectpage.vm b/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
index 6c82bfb..8a608f6 100644
--- a/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
@@ -180,6 +180,8 @@
 			</div>
 		</div>
 		#parse( "azkaban/webapp/servlet/velocity/flowexecutionpanel.vm" )
+		<div id="contextMenu">
+		</div>
 		#parse( "azkaban/webapp/servlet/velocity/messagedialog.vm" )
 	</body>
 	
diff --git a/src/web/css/azkaban-graph.css b/src/web/css/azkaban-graph.css
index c20e6d9..b5b6b57 100644
--- a/src/web/css/azkaban-graph.css
+++ b/src/web/css/azkaban-graph.css
@@ -196,3 +196,52 @@ svg circle.SKIPPED {
 	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%;
+}
\ No newline at end of file
diff --git a/src/web/js/azkaban.context.menu.js b/src/web/js/azkaban.context.menu.js
index 9bf1ef0..7bca798 100644
--- a/src/web/js/azkaban.context.menu.js
+++ b/src/web/js/azkaban.context.menu.js
@@ -18,6 +18,7 @@ azkaban.ContextMenuView = Backbone.View.extend({
 
 		var contextMenu = this.setupMenu(menu);
 		$(contextMenu).css({top: y, left: x});
+		
 		$(this.el).after(contextMenu);
 	},
 	hide : function(evt) {
diff --git a/src/web/js/azkaban.flow.execute.view.js b/src/web/js/azkaban.flow.execute.view.js
index 90319c6..c4de9f4 100644
--- a/src/web/js/azkaban.flow.execute.view.js
+++ b/src/web/js/azkaban.flow.execute.view.js
@@ -525,7 +525,7 @@ var touchDescendents = function(jobid, disable) {
 	executableGraphModel.trigger("change:disabled");
 }
 
-var nodeClickCallback = function(event) {
+var exNodeClickCallback = function(event) {
 	console.log("Node clicked callback");
 	var jobId = event.currentTarget.jobid;
 	var flowId = executableGraphModel.get("flowId");
@@ -556,11 +556,11 @@ var nodeClickCallback = function(event) {
 	contextMenuView.show(event, menu);
 }
 
-var edgeClickCallback = function(event) {
+var exEdgeClickCallback = function(event) {
 	console.log("Edge clicked callback");
 }
 
-var graphClickCallback = function(event) {
+var exGraphClickCallback = function(event) {
 	console.log("Graph clicked callback");
 	var flowId = executableGraphModel.get("flowId");
 	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
@@ -580,7 +580,7 @@ var contextMenuView;
 $(function() {
 	executableGraphModel = new azkaban.GraphModel();
 	flowExecuteDialogView = new azkaban.FlowExecuteDialogView({el:$('#execute-flow-panel'), model: executableGraphModel});
-	svgGraphView = new azkaban.SvgGraphView({el:$('#svgDivCustom'), model: executableGraphModel, topGId:"topG", graphMargin: 10, rightClick: { "node": nodeClickCallback, "edge": edgeClickCallback, "graph": graphClickCallback }});
+	svgGraphView = new azkaban.SvgGraphView({el:$('#svgDivCustom'), model: executableGraphModel, topGId:"topG", graphMargin: 10, rightClick: { "node": exNodeClickCallback, "edge": exEdgeClickCallback, "graph": exGraphClickCallback }});
 	
 	sideMenuDialogView = new azkaban.SideMenuDialogView({el:$('#graphOptions')});
 	editTableView = new azkaban.EditTableView({el:$('#editTable')});
diff --git a/src/web/js/azkaban.flow.extended.view.js b/src/web/js/azkaban.flow.extended.view.js
new file mode 100644
index 0000000..21985b3
--- /dev/null
+++ b/src/web/js/azkaban.flow.extended.view.js
@@ -0,0 +1,67 @@
+azkaban.FlowExtendedViewPanel = Backbone.View.extend({
+	events: {
+		"click .closeInfoPanel" : "handleClosePanel" 
+	},
+	initialize: function(settings) {
+		//this.model.bind('change:flowinfo', this.changeFlowInfo, this);
+		$(this.el).show();
+		$(this.el).draggable({cancel: ".dataContent", containment: "document"});
+		
+		this.extendedViewPanels = {};
+		this.extendedDataModels = {};
+		this.render();
+		$(this.el).hide();
+	},
+	showExtendedView: function(evt) {
+		var event = evt;
+		
+		$(this.el).css({top: evt.pageY, left: evt.pageX});
+		$(this.el).show();
+	},
+	render: function(self) {
+		console.log("Changing title");
+		$(this.el).find(".nodeId").text(this.model.get("id"));
+		$(this.el).find(".nodeType").text(this.model.get("type"));
+		
+		var props = this.model.get("props");
+		var tableBody = $(this.el).find(".dataPropertiesBody");
+		
+		for (var key in props) {
+			var tr = document.createElement("tr");
+			var tdKey = document.createElement("td");
+			var tdValue = document.createElement("td");
+			
+			$(tdKey).text(key);
+			$(tdValue).text(props[key]);
+			
+			$(tr).append(tdKey);
+			$(tr).append(tdValue);
+			
+			$(tableBody).append(tr);
+			
+			var propsTable = $(this.el).find(".dataJobProperties");
+			$(propsTable).resizable({handler: "s"});
+		}
+		
+		if (this.model.get("type") == "flow") {
+			var svgns = "http://www.w3.org/2000/svg";
+			var svgDataFlow = $(this.el).find(".dataFlow");
+			
+			var svgGraph = document.createElementNS(svgns, "svg");
+			$(svgGraph).attr("class", "svgTiny");
+			$(svgDataFlow).append(svgGraph);
+			$(svgDataFlow).resizable();
+			
+			this.innerGraphModel = new azkaban.GraphModel();
+			this.innerGraphModel.set({"data": this.model.get("flow")});
+			
+			this.graphView = new azkaban.SvgGraphView({el: svgDataFlow, model: this.innerGraphModel, render: true, rightClick:  { "node": nodeClickCallback, "graph": graphClickCallback }})
+		}
+		else {
+			$(this.el).find(".dataFlow").hide();
+		}
+	},
+	handleClosePanel: function(self) {
+		$(this.el).hide();
+	}
+});
\ No newline at end of file
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index 46a336f..73945f0 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -275,54 +275,6 @@ azkaban.ExecutionsView = Backbone.View.extend({
 	}
 });
 
-var exNodeClickCallback = function(event) {
-	console.log("Node clicked callback");
-	var jobId = event.currentTarget.jobid;
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
-
-	var menu = [	
-			{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
-			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
-			{break: 1},
-			{title: "Center Job", callback: function() {graphModel.trigger("centerNode", jobId)}}
-	];
-
-	contextMenuView.show(event, menu);
-}
-
-var exJobClickCallback = function(event) {
-	console.log("Node clicked callback");
-	var jobId = event.currentTarget.jobid;
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
-
-	var menu = [	
-			{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
-			{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
-			{break: 1},
-			{title: "Center Job", callback: function() {graphModel.trigger("centerNode", jobId)}}
-	];
-
-	contextMenuView.show(event, menu);
-}
-
-var exEdgeClickCallback = function(event) {
-	console.log("Edge clicked callback");
-}
-
-var exGraphClickCallback = function(event) {
-	console.log("Graph clicked callback");
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
-
-	var menu = [	
-		{title: "Open Flow...", callback: function() {window.location.href=requestURL;}},
-		{title: "Open Flow in New Window...", callback: function() {window.open(requestURL);}},
-		{break: 1},
-		{title: "Center Graph", callback: function() {graphModel.trigger("resetPanZoom");}}
-	];
-	
-	contextMenuView.show(event, menu);
-}
-
 var graphModel;
 azkaban.GraphModel = Backbone.Model.extend({});
 
@@ -339,8 +291,8 @@ $(function() {
 	flowTabView = new azkaban.FlowTabView({el:$( '#headertabs'), selectedView: selected });
 
 	graphModel = new azkaban.GraphModel();
-	mainSvgGraphView = new azkaban.SvgGraphView({el:$('#svgDiv'), model: graphModel, rightClick:  { "node": exNodeClickCallback, "edge": exEdgeClickCallback, "graph": exGraphClickCallback }});
-	jobsListView = new azkaban.JobListView({el:$('#jobList'), model: graphModel, contextMenuCallback: exJobClickCallback});
+	mainSvgGraphView = new azkaban.SvgGraphView({el:$('#svgDiv'), model: graphModel, rightClick:  { "node": nodeClickCallback, "edge": edgeClickCallback, "graph": graphClickCallback }});
+	jobsListView = new azkaban.JobListView({el:$('#jobList'), model: graphModel, contextMenuCallback: jobClickCallback});
 	
 	var requestURL = contextURL + "/manager";
 
@@ -362,32 +314,7 @@ $(function() {
 	      requestURL,
 	      {"project": projectName, "ajax":"fetchflowgraph", "flow":flowId},
 	      function(data) {
-	      	  // Create the nodes
-	      	  var nodes = {};
-	      	  for (var i=0; i < data.nodes.length; ++i) {
-	      	  	var node = data.nodes[i];
-	      	  	nodes[node.id] = node;
-	      	  }
-	      	  for (var i=0; i < data.edges.length; ++i) {
-	      	  	var edge = data.edges[i];
-	      	  	var fromNode = nodes[edge.from];
-	      	  	var toNode = nodes[edge.target];
-	      	  	
-	      	  	if (!fromNode.outNodes) {
-	      	  		fromNode.outNodes = {};
-	      	  	}
-	      	  	fromNode.outNodes[toNode.id] = toNode;
-	      	  	
-	      	  	if (!toNode.inNodes) {
-	      	  		toNode.inNodes = {};
-	      	  	}
-	      	  	toNode.inNodes[fromNode.id] = fromNode;
-	      	  }
-	      
-	          console.log("data fetched");
-	          graphModel.set({data: data});
-	          graphModel.set({nodes: nodes});
-	          graphModel.set({disabled: {}});
+	    	  createModelFromAjaxCall(data, graphModel);
 	          graphModel.trigger("change:graph");
 	          
 	          // Handle the hash changes here so the graph finishes rendering first.
diff --git a/src/web/js/azkaban.svg.graph.helper.js b/src/web/js/azkaban.svg.graph.helper.js
new file mode 100644
index 0000000..67ee1f7
--- /dev/null
+++ b/src/web/js/azkaban.svg.graph.helper.js
@@ -0,0 +1,152 @@
+var extendedViewPanels = {};
+var extendedDataModels = {};
+var openJobDisplayCallback = function(nodeId, flowId, evt) {
+	console.log("Open up data");
+	
+	var nodeInfoPanelID = flowId + ":" + nodeId + "-info";
+	if ($("#" + nodeInfoPanelID).length) {
+		$("#flowInfoBase").before(cloneStuff);
+		extendedViewPanels[nodeInfoPanelID].showExtendedView(evt);
+		return;
+	}
+	
+	var cloneStuff = $("#flowInfoBase").clone();
+	$(cloneStuff).attr("id", nodeInfoPanelID);
+	
+	$("#flowInfoBase").before(cloneStuff);
+	var requestURL = contextURL + "/manager";
+	
+	$.get(
+      requestURL,
+      {"project": projectName, "ajax":"fetchflownodedata", "flow":flowId, "node": nodeId},
+      function(data) {
+  		var graphModel = new azkaban.GraphModel();
+  		graphModel.set({id: data.id, flow: data.flowData, type: data.type, props: data.props});
+
+  		var flowData = data.flowData;
+  		if (flowData) {
+  			createModelFromAjaxCall(flowData, graphModel);
+  		}
+  		
+  		var backboneView = new azkaban.FlowExtendedViewPanel({el:cloneStuff, model: graphModel});
+  		extendedViewPanels[nodeInfoPanelID] = backboneView;
+  		extendedDataModels[nodeInfoPanelID] = graphModel;
+  		backboneView.showExtendedView(evt);
+      },
+      "json"
+    );
+}
+
+var createModelFromAjaxCall = function(data, model) {
+	  var nodes = {};
+  	  for (var i=0; i < data.nodes.length; ++i) {
+  	  	var node = data.nodes[i];
+  	  	nodes[node.id] = node;
+  	  }
+  	  for (var i=0; i < data.edges.length; ++i) {
+  	  	var edge = data.edges[i];
+  	  	var fromNode = nodes[edge.from];
+  	  	var toNode = nodes[edge.target];
+  	  	
+  	  	if (!fromNode.outNodes) {
+  	  		fromNode.outNodes = {};
+  	  	}
+  	  	fromNode.outNodes[toNode.id] = toNode;
+  	  	
+  	  	if (!toNode.inNodes) {
+  	  		toNode.inNodes = {};
+  	  	}
+  	  	toNode.inNodes[fromNode.id] = fromNode;
+  	  }
+  
+      console.log("data fetched");
+      model.set({data: data});
+      model.set({nodes: nodes});
+      model.set({disabled: {}});
+}
+
+var nodeClickCallback = function(event, model, type) {
+	console.log("Node clicked callback");
+	var jobId = event.currentTarget.jobid;
+	var flowId = model.get("flowId");
+	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
+
+	if (event.currentTarget.jobtype == "flow") {
+		var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + event.currentTarget.flowId;
+		menu = [
+				{title: "View Flow...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+				{break: 1},
+				{title: "Open Flow...", callback: function() {window.location.href=flowRequestURL;}},
+				{title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}},
+				{break: 1},
+				{title: "Open Properties...", callback: function() {window.location.href=requestURL;}},
+				{title: "Open Properties in New Window...", callback: function() {window.open(requestURL);}},
+				{break: 1},
+				{title: "Center Flow", callback: function() {model.trigger("centerNode", jobId)}}
+		];
+	}
+	else {
+		menu = [
+				{title: "View Job...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+				{break: 1},
+				{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
+				{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+				{break: 1},
+				{title: "Center Job", callback: function() {model.trigger("centerNode", jobId)}}
+		];
+	}
+	contextMenuView.show(event, menu);
+}
+
+var jobClickCallback = function(event, model) {
+	console.log("Node clicked callback");
+	var jobId = event.currentTarget.jobid;
+	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
+
+	var menu;
+	if (event.currentTarget.jobtype == "flow") {
+		var flowRequestURL = contextURL + "/manager?project=" + projectName + "&flow=" + event.currentTarget.flowId;
+		menu = [
+				{title: "View Flow...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+				{break: 1},
+				{title: "Open Flow...", callback: function() {window.location.href=flowRequestURL;}},
+				{title: "Open Flow in New Window...", callback: function() {window.open(flowRequestURL);}},
+				{break: 1},
+				{title: "Open Properties...", callback: function() {window.location.href=requestURL;}},
+				{title: "Open Properties in New Window...", callback: function() {window.open(requestURL);}},
+				{break: 1},
+				{title: "Center Flow", callback: function() {model.trigger("centerNode", jobId)}}
+		];
+	}
+	else {
+		menu = [
+				{title: "View Job...", callback: function() {openJobDisplayCallback(jobId, flowId, event)}},
+				{break: 1},
+				{title: "Open Job...", callback: function() {window.location.href=requestURL;}},
+				{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
+				{break: 1},
+				{title: "Center Job", callback: function() {graphModel.trigger("centerNode", jobId)}}
+		];
+	}
+	contextMenuView.show(event, menu);
+}
+
+var edgeClickCallback = function(event, model) {
+	console.log("Edge clicked callback");
+}
+
+var graphClickCallback = function(event, model) {
+	console.log("Graph clicked callback");
+	var jobId = event.currentTarget.jobid;
+	var flowId = model.get("flowId");
+	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
+
+	var menu = [	
+		{title: "Open Flow...", callback: function() {window.location.href=requestURL;}},
+		{title: "Open Flow in New Window...", callback: function() {window.open(requestURL);}},
+		{break: 1},
+		{title: "Center Graph", callback: function() {model.trigger("resetPanZoom");}}
+	];
+	
+	contextMenuView.show(event, menu);
+}
diff --git a/src/web/js/azkaban.svg.graph.view.js b/src/web/js/azkaban.svg.graph.view.js
index 3703e27..1e0f443 100644
--- a/src/web/js/azkaban.svg.graph.view.js
+++ b/src/web/js/azkaban.svg.graph.view.js
@@ -57,6 +57,10 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		}
 
 		$(svg).svgNavigate();
+		
+		if (settings.render) {
+			this.render();
+		}
 	},
 	initializeDefs: function(self) {
 		var def = document.createElementNS(svgns, 'defs');
@@ -133,7 +137,7 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			this.drawEdge(this, edges[i]);
 		}
 		
-		this.model.set({"nodes": this.nodes, "edges": edges});
+		this.model.set({"flowId":data.flowId, "nodes": this.nodes, "edges": edges});
 		
 		var margin = this.graphMargin;
 		bounds.minX = bounds.minX ? bounds.minX - margin : -margin;
@@ -142,7 +146,13 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		bounds.maxY = bounds.maxY ? bounds.maxY + margin : margin;
 		
 		this.assignInitialStatus(self);
-		this.handleDisabledChange(self);
+		
+		if (this.model.get("disabled")) {
+			this.handleDisabledChange(self);
+		}
+		else {
+			this.model.set({"disabled":[]})
+		}
 		this.graphBounds = bounds;
 		this.resetPanZoom(0);
 	},
@@ -232,13 +242,13 @@ azkaban.SvgGraphView = Backbone.View.extend({
 			var callbacks = this.rightClick;
 			var currentTarget = self.currentTarget;
 			if (callbacks.node && currentTarget.jobid) {
-				callbacks.node(self);
+				callbacks.node(self, this.model);
 			}
 			else if (callbacks.edge && (currentTarget.nodeName == "polyline" || currentTarget.nodeName == "line")) {
-				callbacks.edge(self);
+				callbacks.edge(self, this.model);
 			}
 			else if (callbacks.graph) {
-				callbacks.graph(self);
+				callbacks.graph(self, this.model);
 			}
 			return false;
 		}
@@ -350,6 +360,8 @@ azkaban.SvgGraphView = Backbone.View.extend({
 		innerG.appendChild(flowIdText);
 		innerG.appendChild(iconNode);
 		innerG.jobid = node.id;
+		innerG.jobtype = "flow";
+		innerG.flowId = node.flowId;
 
 		nodeG.appendChild(innerG);
 		self.mainG.appendChild(nodeG);