azkaban-developers

Merge pull request #179 from davidzchen/robust-flow-summary Flow

2/14/2014 8:38:44 PM

Details

diff --git a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
index 3ecc9cf..c8cbc5c 100644
--- a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
@@ -1,12 +1,12 @@
 #*
  * Copyright 2012 LinkedIn Corp.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -14,16 +14,18 @@
  * the License.
 *#
 
-<!DOCTYPE html> 
+<!DOCTYPE html>
 <html lang="en">
 	<head>
 
 #parse("azkaban/webapp/servlet/velocity/style.vm")
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
 #parse("azkaban/webapp/servlet/velocity/svgflowincludes.vm")
+    <script type="text/javascript" src="${context}/js/raphael.min.js"></script>
+    <script type="text/javascript" src="${context}/js/morris.min.js"></script>
 		<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/dust-full-2.2.3.min.js"></script>
 		<script type="text/javascript" src="${context}/js/flowstats.js"></script>
 		<script type="text/javascript" src="${context}/js/flowstats-no-data.js"></script>
@@ -43,7 +45,7 @@
 			var flowId = "${flowid}";
 			var execId = "${execid}";
 		</script>
-
+		<link rel="stylesheet" type="text/css" href="${context}/css/morris.css" />
 		<link rel="stylesheet" type="text/css" href="${context}/css/jquery-ui-1.10.1.custom.css" />
 	</head>
 	<body>
@@ -97,7 +99,7 @@
 		<div class="container-full">
 
   #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
-	
+
 	## Tabs and buttons.
 
 			<ul class="nav nav-tabs nav-sm" id="headertabs">
@@ -116,7 +118,7 @@
 	## Graph View
 
 	#parse ("azkaban/webapp/servlet/velocity/flowgraphview.vm")
-	
+
 	## Job List View
 
     <div class="container-full" id="jobListView">
@@ -178,7 +180,7 @@
         </div>
 			</div><!-- /.row -->
     </div><!-- /.container-fill -->
-	
+
 	## Error message message dialog.
 
     <div class="container-full">
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index acb0573..0394da9 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
@@ -1,12 +1,12 @@
 #*
  * Copyright 2012 LinkedIn Corp.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -14,7 +14,7 @@
  * the License.
 *#
 
-<!DOCTYPE html> 
+<!DOCTYPE html>
 <html lang="en">
 	<head>
 
@@ -25,12 +25,12 @@
     <script type="text/javascript" src="${context}/js/bootstrap-datetimepicker.min.js"></script>
     <script type="text/javascript" src="${context}/js/raphael.min.js"></script>
     <script type="text/javascript" src="${context}/js/morris.min.js"></script>
-		
+
     <script type="text/javascript" src="${context}/js/dust-full-2.2.3.min.js"></script>
 		<script type="text/javascript" src="${context}/js/flowsummary.js"></script>
 		<script type="text/javascript" src="${context}/js/flowstats-no-data.js"></script>
 		<script type="text/javascript" src="${context}/js/flowstats.js"></script>
-		
+
 		<script type="text/javascript" src="${context}/js/azkaban/view/time-graph.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban/util/schedule.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban/view/schedule-sla.js"></script>
@@ -43,7 +43,7 @@
 			var timezone = "${timezone}";
 			var errorMessage = null;
 			var successMessage = null;
-			
+
 			var projectId = ${project.id};
 			var projectName = "${project.name}";
 			var flowId = "${flowid}";
@@ -60,7 +60,7 @@
 #if ($errorMsg)
   #parse ("azkaban/webapp/servlet/velocity/errormsg.vm")
 #else
-	
+
 	## Page header.
 
 		<div class="az-page-header page-header-bare">
@@ -87,9 +87,9 @@
     </div>
 
 		<div class="container-full">
-      
+
   #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
-  
+
   ## Tabs
 
 			<ul class="nav nav-tabs nav-sm" id="headertabs">
@@ -98,7 +98,7 @@
 				<li id="summaryViewLink"><a href="#summary">Summary</a></li>
 			</ul>
     </div>
-					
+
 	## Graph view.
 
 	#parse ("azkaban/webapp/servlet/velocity/flowgraphview.vm")
@@ -149,7 +149,7 @@
           <div class="col-xs-12">
             <div class="callout callout-info">
               <h4>Analyze last run</h4>
-              <p>Analyze the last run for aggregate performance statistics. <strong>Note:</strong> this may take a few minutes, especially if your flow is large.</p>
+              <p>Analyze the last run for aggregate performance statistics. Note: this may take a few minutes, especially if your flow is large.</p>
               <p>
                 <button type="button" id="analyze-btn" class="btn btn-primary">Analyze</button>
               </p>
diff --git a/src/java/azkaban/webapp/servlet/velocity/jobhistorypage.vm b/src/java/azkaban/webapp/servlet/velocity/jobhistorypage.vm
index b1838bb..270f8e1 100644
--- a/src/java/azkaban/webapp/servlet/velocity/jobhistorypage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/jobhistorypage.vm
@@ -1,12 +1,12 @@
 #*
  * Copyright 2012 LinkedIn Corp.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -14,7 +14,7 @@
  * the License.
 *#
 
-<!DOCTYPE html> 
+<!DOCTYPE html>
 <html lang="en">
 	<head>
 
@@ -32,7 +32,7 @@
 			var timezone = "${timezone}";
 			var errorMessage = null;
 			var successMessage = null;
-			
+
 			var projectId = "$projectId";
 			var projectName = "$projectName";
 			var jobName = "$jobid";
@@ -48,7 +48,7 @@
 #if ($errorMsg)
   #parse ("azkaban/webapp/servlet/velocity/errormsg.vm")
 #else
-	
+
 	## Page header
 
 		<div class="az-page-header page-header-bare">
@@ -66,7 +66,7 @@
     </div>
 
 		<div class="container-full">
-	
+
   #parse ("azkaban/webapp/servlet/velocity/alerts.vm")
 
   ## Time graph and job history table.
@@ -84,7 +84,7 @@
                 <th class="flowid">Flow</th>
                 <th class="date">Start Time</th>
                 <th class="date">End Time</th>
-                <th class="elapse">Elapse</th>		
+                <th class="elapse">Elapse</th>
                 <th class="status">Status</th>
                 <th class="logs">Logs</th>
               </tr>

src/tl/flowstats.tl 22(+22 -0)

diff --git a/src/tl/flowstats.tl b/src/tl/flowstats.tl
index 8fffebe..5276ce8 100644
--- a/src/tl/flowstats.tl
+++ b/src/tl/flowstats.tl
@@ -1,3 +1,25 @@
+      {?histogram}
+      <div class="row">
+        <div class="col-xs-12">
+          <div class="well well-clear well-sm">
+            <div id="job-histogram"></div>
+          </div>
+        </div>
+      </div>
+      {/histogram}
+
+      {?warnings}
+      <div class="alert alert-warning">
+        <h4>Warnings</h4>
+        <p>These stats may have reduced accuracy due to the following missing information:</p>
+        <ul>
+        {#warnings}
+          <li>{.}</li>
+        {/warnings}
+        </ul>
+      </div>
+      {/warnings}
+
       <div class="row">
         <div class="col-xs-12">
           <h4>Resources</h4>
diff --git a/src/tl/flowsummary.tl b/src/tl/flowsummary.tl
index e4f525f..82627d6 100644
--- a/src/tl/flowsummary.tl
+++ b/src/tl/flowsummary.tl
@@ -14,7 +14,7 @@
           </table>
         </div>
       </div>
-			
+
       <div class="row">
         <div class="col-xs-12">
           <h3>
@@ -46,12 +46,12 @@
                 <td class="property-key">SLA</td>
                 <td class="property-value-half">
                 {?schedule.slaOptions}
-                  true 
-                {:else} 
-                  false 
+                  true
+                {:else}
+                  false
                 {/schedule.slaOptions}
                   <div class="pull-right">
-                    <button type="button" id="addSlaBtn" class="btn btn-xs btn-primary" onclick="slaView.initFromSched({schedule.scheduleId}, '{flowName}')" >Set SLA</button>
+                    <button type="button" id="addSlaBtn" class="btn btn-xs btn-primary" onclick="slaView.initFromSched({schedule.scheduleId}, '{flowName}')" >View/Set SLA</button>
                   </div>
                 </td>
               </tr>
diff --git a/src/web/js/azkaban/view/exflow.js b/src/web/js/azkaban/view/exflow.js
index 6129676..7c2548c 100644
--- a/src/web/js/azkaban/view/exflow.js
+++ b/src/web/js/azkaban/view/exflow.js
@@ -1,12 +1,12 @@
 /*
  * Copyright 2012 LinkedIn Corp.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -35,16 +35,16 @@ azkaban.StatusView = Backbone.View.extend({
 	},
 	render: function(evt) {
 		var data = this.model.get("data");
-		
+
 		var user = data.submitUser;
 		$("#submitUser").text(user);
-		
+
 		this.statusUpdate(evt);
 	},
-	
+
 	statusUpdate: function(evt) {
 		var data = this.model.get("data");
-		
+
 		statusItem = $("#flowStatus");
 		for (var j = 0; j < statusList.length; ++j) {
 			var status = statusList[j];
@@ -52,27 +52,27 @@ azkaban.StatusView = Backbone.View.extend({
 		}
 		$("#flowStatus").addClass(data.status);
 		$("#flowStatus").text(data.status);
-		
+
 		var startTime = data.startTime;
 		var endTime = data.endTime;
-		
+
 		if (!startTime || startTime == -1) {
 			$("#startTime").text("-");
 		}
 		else {
 			var date = new Date(startTime);
 			$("#startTime").text(getDateFormat(date));
-			
+
 			var lastTime = endTime;
 			if (endTime == -1) {
 				var currentDate = new Date();
 				lastTime = currentDate.getTime();
 			}
-			
+
 			var durationString = getDuration(startTime, lastTime);
 			$("#duration").text(durationString);
 		}
-		
+
 		if (!endTime || endTime == -1) {
 			$("#endTime").text("-");
 		}
@@ -96,17 +96,17 @@ azkaban.FlowTabView = Backbone.View.extend({
 		"click #resumebtn": "handleResumeClick",
 		"click #retrybtn": "handleRetryClick"
 	},
-	
+
 	initialize: function(settings) {
 		$("#cancelbtn").hide();
 		$("#executebtn").hide();
 		$("#pausebtn").hide();
 		$("#resumebtn").hide();
 		$("#retrybtn").hide();
-	
+
 		this.model.bind('change:graph', this.handleFlowStatusChange, this);
 		this.model.bind('change:update', this.handleFlowStatusChange, this);
-	
+
 		var selectedView = settings.selectedView;
 		if (selectedView == "jobslist") {
 			this.handleJobslistLinkClick();
@@ -115,60 +115,60 @@ azkaban.FlowTabView = Backbone.View.extend({
 			this.handleGraphLinkClick();
 		}
 	},
-	
+
 	render: function() {
 		console.log("render graph");
 	},
-	
+
 	handleGraphLinkClick: function(){
 		$("#jobslistViewLink").removeClass("active");
 		$("#graphViewLink").addClass("active");
 		$("#flowLogViewLink").removeClass("active");
 		$("#statsViewLink").removeClass("active");
-		
+
 		$("#jobListView").hide();
 		$("#graphView").show();
 		$("#flowLogView").hide();
 		$("#statsView").hide();
 	},
-	
+
 	handleJobslistLinkClick: function() {
 		$("#graphViewLink").removeClass("active");
 		$("#jobslistViewLink").addClass("active");
 		$("#flowLogViewLink").removeClass("active");
 		$("#statsViewLink").removeClass("active");
-		
+
 		$("#graphView").hide();
 		$("#jobListView").show();
 		$("#flowLogView").hide();
 		$("#statsView").hide();
 	},
-	
+
 	handleLogLinkClick: function() {
 		$("#graphViewLink").removeClass("active");
 		$("#jobslistViewLink").removeClass("active");
 		$("#flowLogViewLink").addClass("active");
 		$("#statsViewLink").removeClass("active");
-		
+
 		$("#graphView").hide();
 		$("#jobListView").hide();
 		$("#flowLogView").show();
 		$("#statsView").hide();
 	},
-	
+
   handleStatsLinkClick: function() {
 		$("#graphViewLink").removeClass("active");
 		$("#jobslistViewLink").removeClass("active");
 		$("#flowLogViewLink").removeClass("active");
 		$("#statsViewLink").addClass("active");
-		
+
 		$("#graphView").hide();
 		$("#jobListView").hide();
 		$("#flowLogView").hide();
     statsView.show();
 		$("#statsView").show();
 	},
-	
+
 	handleFlowStatusChange: function() {
 		var data = this.model.get("data");
 		$("#cancelbtn").hide();
@@ -203,7 +203,7 @@ azkaban.FlowTabView = Backbone.View.extend({
 			$("#executebtn").show();
 		}
 	},
-	
+
 	handleCancelClick: function(evt) {
 		var requestURL = contextURL + "/executor";
 		var requestData = {"execid": execId, "ajax": "cancelFlow"};
@@ -219,7 +219,7 @@ azkaban.FlowTabView = Backbone.View.extend({
 		};
 		ajaxCall(requestURL, requestData, successHandler);
 	},
-	
+
 	handleRetryClick: function(evt) {
 		var graphData = graphModel.get("data");
 		var requestURL = contextURL + "/executor";
@@ -236,11 +236,11 @@ azkaban.FlowTabView = Backbone.View.extend({
 		};
 		ajaxCall(requestURL, requestData, successHandler);
 	},
-	
+
 	handleRestartClick: function(evt) {
 		console.log("handleRestartClick");
 		var data = graphModel.get("data");
-		
+
 		var executingData = {
 			project: projectName,
 			ajax: "executeFlow",
@@ -250,7 +250,7 @@ azkaban.FlowTabView = Backbone.View.extend({
 		};
 		flowExecuteDialogView.show(executingData);
 	},
-	
+
 	handlePauseClick: function(evt) {
 		var requestURL = contextURL + "/executor";
 		var requestData = {"execid": execId, "ajax":"pauseFlow"};
@@ -266,7 +266,7 @@ azkaban.FlowTabView = Backbone.View.extend({
 		};
 		ajaxCall(requestURL, requestData, successHandler);
 	},
-	
+
 	handleResumeClick: function(evt) {
 		var requestURL = contextURL + "/executor";
 		var requestData = {"execid": execId, "ajax":"resumeFlow"};
@@ -304,17 +304,17 @@ azkaban.FlowLogView = Backbone.View.extend({
 	},
 	handleUpdate: function(evt) {
 		var offset = this.model.get("offset");
-		var requestURL = contextURL + "/executor"; 
+		var requestURL = contextURL + "/executor";
 		var model = this.model;
 		console.log("fetchLogs offset is " + offset)
 
 		$.ajax({
-			async: false, 
+			async: false,
 			url: requestURL,
 			data: {
-				"execid": execId, 
-				"ajax": "fetchExecFlowLogs", 
-				"offset": offset, 
+				"execid": execId,
+				"ajax": "fetchExecFlowLogs",
+				"offset": offset,
 				"length": 50000
 			},
 			success: function(data) {
@@ -346,7 +346,7 @@ var statsView;
 azkaban.StatsView = Backbone.View.extend({
 	events: {
 	},
-	
+
   initialize: function(settings) {
     this.model.bind('change:graph', this.statusUpdate, this);
     this.model.bind('change:update', this.statusUpdate, this);
@@ -385,22 +385,22 @@ var updateStatus = function(updateTime) {
 	var requestURL = contextURL + "/executor";
 	var oldData = graphModel.get("data");
 	var nodeMap = graphModel.get("nodeMap");
-	
+
 	if (!updateTime) {
 		updateTime = oldData.updateTime ? oldData.updateTime : 0;
 	}
 
 	var requestData = {
-		"execid": execId, 
-		"ajax": "fetchexecflowupdate", 
+		"execid": execId,
+		"ajax": "fetchexecflowupdate",
 		"lastUpdateTime": updateTime
 	};
-	
+
 	var successHandler = function(data) {
 		console.log("data updated");
 		if (data.updateTime) {
 			updateGraph(oldData, data);
-	
+
 			graphModel.set({"update": data});
 			graphModel.trigger("change:update");
 		}
@@ -415,12 +415,12 @@ var updateGraph = function(data, update) {
 	data.updateTime = update.updateTime;
 	data.status = update.status;
 	update.changedNode = data;
-	
+
 	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);
 		}
 	}
@@ -429,17 +429,17 @@ var updateGraph = function(data, update) {
 var updateTime = -1;
 var updaterFunction = function() {
 	var oldData = graphModel.get("data");
-	var keepRunning = 
-			oldData.status != "SUCCEEDED" && 
-			oldData.status != "FAILED" && 
+	var keepRunning =
+			oldData.status != "SUCCEEDED" &&
+			oldData.status != "FAILED" &&
 			oldData.status != "KILLED";
 
 	if (keepRunning) {
 		updateStatus();
 
 		var data = graphModel.get("data");
-		if (data.status == "UNKNOWN" || 
-			data.status == "WAITING" || 
+		if (data.status == "UNKNOWN" ||
+			data.status == "WAITING" ||
 			data.status == "PREPARING") {
 			setTimeout(function() {updaterFunction();}, 1000);
 		}
@@ -459,9 +459,9 @@ var updaterFunction = function() {
 
 var logUpdaterFunction = function() {
 	var oldData = graphModel.get("data");
-	var keepRunning = 
-			oldData.status != "SUCCEEDED" && 
-			oldData.status != "FAILED" && 
+	var keepRunning =
+			oldData.status != "SUCCEEDED" &&
+			oldData.status != "FAILED" &&
 			oldData.status != "KILLED";
 	if (keepRunning) {
 		// update every 30 seconds for the logs until finished
@@ -479,7 +479,7 @@ var exNodeClickCallback = function(event) {
 	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
 	var visualizerURL = contextURL + "/pigvisualizer?execid=" + execId + "&jobid=" + jobId;
 
-	var menu = [	
+	var menu = [
 		{title: "Open Job...", callback: function() {window.location.href = requestURL;}},
 		{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
 		{title: "Visualize Job...", callback: function() {window.location.href = visualizerURL;}}
@@ -494,7 +494,7 @@ var exJobClickCallback = function(event) {
 	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId + "&job=" + jobId;
 	var visualizerURL = contextURL + "/pigvisualizer?execid=" + execId + "&jobid=" + jobId;
 
-	var menu = [	
+	var menu = [
 		{title: "Open Job...", callback: function() {window.location.href = requestURL;}},
 		{title: "Open Job in New Window...", callback: function() {window.open(requestURL);}},
 		{title: "Visualize Job...", callback: function() {window.location.href = visualizerURL;}}
@@ -511,13 +511,13 @@ var exGraphClickCallback = function(event) {
 	console.log("Graph clicked callback");
 	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowId;
 
-	var menu = [	
+	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);
 }
 
@@ -526,68 +526,69 @@ var flowStatsModel;
 
 $(function() {
 	var selected;
-	
+
 	graphModel = new azkaban.GraphModel();
 	logModel = new azkaban.LogModel();
-	
+
 	flowTabView = new azkaban.FlowTabView({
-		el: $('#headertabs'), 
+		el: $('#headertabs'),
 		model: graphModel
 	});
-	
+
 	mainSvgGraphView = new azkaban.SvgGraphView({
-		el: $('#svgDiv'), 
-		model: graphModel, 
-		rightClick:	{ 
-			"node": nodeClickCallback, 
-			"edge": edgeClickCallback, 
-			"graph": graphClickCallback 
+		el: $('#svgDiv'),
+		model: graphModel,
+		rightClick:	{
+			"node": nodeClickCallback,
+			"edge": edgeClickCallback,
+			"graph": graphClickCallback
 		}
 	});
-	
+
 	jobsListView = new azkaban.JobListView({
-		el: $('#joblist-panel'), 
-		model: graphModel, 
+		el: $('#joblist-panel'),
+		model: graphModel,
 		contextMenuCallback: jobClickCallback
 	});
-	
+
 	flowLogView = new azkaban.FlowLogView({
-		el: $('#flowLogView'), 
+		el: $('#flowLogView'),
 		model: logModel
 	});
-	
+
 	statusView = new azkaban.StatusView({
-		el: $('#flow-status'), 
+		el: $('#flow-status'),
 		model: graphModel
 	});
-  
+
   flowStatsModel = new azkaban.FlowStatsModel();
 	flowStatsView = new azkaban.FlowStatsView({
 		el: $('#flow-stats-container'),
-		model: flowStatsModel
+		model: flowStatsModel,
+    histogram: false
 	});
 
   statsView = new azkaban.StatsView({
-		el: $('#statsView'), 
+		el: $('#statsView'),
 		model: graphModel
 	});
-	
+
 	executionListView = new azkaban.ExecutionListView({
-		el: $('#jobListView'), 
+		el: $('#jobListView'),
 		model: graphModel
 	});
-	
+
 	var requestURL = contextURL + "/executor";
 	var requestData = {"execid": execId, "ajax":"fetchexecflow"};
 	var successHandler = function(data) {
 		console.log("data fetched");
 		graphModel.addFlow(data);
 		graphModel.trigger("change:graph");
-		
+
 		updateTime = Math.max(updateTime, data.submitTime);
 		updateTime = Math.max(updateTime, data.startTime);
 		updateTime = Math.max(updateTime, data.endTime);
-		
+
 		if (window.location.hash) {
 			var hash = window.location.hash;
 			if (hash == "#jobslist") {
diff --git a/src/web/js/azkaban/view/flow.js b/src/web/js/azkaban/view/flow.js
index 3934055..6ed8d89 100644
--- a/src/web/js/azkaban/view/flow.js
+++ b/src/web/js/azkaban/view/flow.js
@@ -1,12 +1,12 @@
 /*
  * Copyright 2012 LinkedIn Corp.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -18,7 +18,7 @@ $.namespace('azkaban');
 
 var handleJobMenuClick = function(action, el, pos) {
 	var jobid = el[0].jobid;
-	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + 
+	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" +
 			flowId + "&job=" + jobid;
 	if (action == "open") {
 		window.location.href = requestURL;
@@ -35,7 +35,7 @@ azkaban.FlowTabView = Backbone.View.extend({
 		"click #executionsViewLink": "handleExecutionLinkClick",
 		"click #summaryViewLink": "handleSummaryLinkClick"
 	},
-	
+
 	initialize: function(settings) {
 		var selectedView = settings.selectedView;
 		if (selectedView == "executions") {
@@ -45,26 +45,26 @@ azkaban.FlowTabView = Backbone.View.extend({
 			this.handleGraphLinkClick();
 		}
 	},
-	
+
 	render: function() {
 		console.log("render graph");
 	},
-	
+
 	handleGraphLinkClick: function(){
 		$("#executionsViewLink").removeClass("active");
 		$("#graphViewLink").addClass("active");
 		$('#summaryViewLink').removeClass('active');
-		
+
 		$("#executionsView").hide();
 		$("#graphView").show();
 		$('#summaryView').hide();
 	},
-	
+
 	handleExecutionLinkClick: function() {
 		$("#graphViewLink").removeClass("active");
 		$("#executionsViewLink").addClass("active");
 		$('#summaryViewLink').removeClass('active');
-		
+
 		$("#graphView").hide();
 		$("#executionsView").show();
 		$('#summaryView').hide();
@@ -90,35 +90,35 @@ azkaban.ExecutionsView = Backbone.View.extend({
 	events: {
 		"click #pageSelection li": "handleChangePageSelection"
 	},
-	
+
 	initialize: function(settings) {
 		this.model.bind('change:view', this.handleChangeView, this);
 		this.model.bind('render', this.render, this);
 		this.model.set({page: 1, pageSize: 16});
 		this.model.bind('change:page', this.handlePageChange, this);
 	},
-	
+
 	render: function(evt) {
 		console.log("render");
 		// Render page selections
 		var tbody = $("#execTableBody");
 		tbody.empty();
-		
+
 		var executions = this.model.get("executions");
 		for (var i = 0; i < executions.length; ++i) {
 			var row = document.createElement("tr");
-			
+
 			var tdId = document.createElement("td");
 			var execA = document.createElement("a");
 			$(execA).attr("href", contextURL + "/executor?execid=" + executions[i].execId);
 			$(execA).text(executions[i].execId);
 			tdId.appendChild(execA);
 			row.appendChild(tdId);
-			
+
 			var tdUser = document.createElement("td");
 			$(tdUser).text(executions[i].submitUser);
 			row.appendChild(tdUser);
-			
+
 			var startTime = "-";
 			if (executions[i].startTime != -1) {
 				var startDateTime = new Date(executions[i].startTime);
@@ -128,7 +128,7 @@ azkaban.ExecutionsView = Backbone.View.extend({
 			var tdStartTime = document.createElement("td");
 			$(tdStartTime).text(startTime);
 			row.appendChild(tdStartTime);
-			
+
 			var endTime = "-";
 			var lastTime = executions[i].endTime;
 			if (executions[i].endTime != -1) {
@@ -142,11 +142,11 @@ azkaban.ExecutionsView = Backbone.View.extend({
 			var tdEndTime = document.createElement("td");
 			$(tdEndTime).text(endTime);
 			row.appendChild(tdEndTime);
-			
+
 			var tdElapsed = document.createElement("td");
 			$(tdElapsed).text( getDuration(executions[i].startTime, lastTime));
 			row.appendChild(tdElapsed);
-			
+
 			var tdStatus = document.createElement("td");
 			var status = document.createElement("div");
 			$(status).addClass("status");
@@ -160,22 +160,22 @@ azkaban.ExecutionsView = Backbone.View.extend({
 
 			tbody.append(row);
 		}
-		
+
 		this.renderPagination(evt);
 	},
-	
+
 	renderPagination: function(evt) {
 		var total = this.model.get("total");
 		total = total? total : 1;
 		var pageSize = this.model.get("pageSize");
 		var numPages = Math.ceil(total / pageSize);
-		
+
 		this.model.set({"numPages": numPages});
 		var page = this.model.get("page");
-		
+
 		//Start it off
 		$("#pageSelection .active").removeClass("active");
-		
+
 		// Disable if less than 5
 		console.log("Num pages " + numPages)
 		var i = 1;
@@ -185,7 +185,7 @@ azkaban.ExecutionsView = Backbone.View.extend({
 		for (; i <= 5; ++i) {
 			$("#page" + i).addClass("disabled");
 		}
-		
+
 		// Disable prev/next if necessary.
 		if (page > 1) {
 			$("#previous").removeClass("disabled");
@@ -195,7 +195,7 @@ azkaban.ExecutionsView = Backbone.View.extend({
 		else {
 			$("#previous").addClass("disabled");
 		}
-		
+
 		if (page < numPages) {
 			$("#next")[0].page = page + 1;
 			$("#next").removeClass("disabled");
@@ -205,7 +205,7 @@ azkaban.ExecutionsView = Backbone.View.extend({
 			$("#next")[0].page = page + 1;
 			$("#next").addClass("disabled");
 		}
-		
+
 		// Selection is always in middle unless at barrier.
 		var startPage = 0;
 		var selectionPosition = 0;
@@ -235,14 +235,14 @@ azkaban.ExecutionsView = Backbone.View.extend({
 		for (var j = 0; j < 5; ++j) {
 			var realPage = startPage + j;
 			var elementId = "#page" + (j+1);
-			
+
 			$(elementId)[0].page = realPage;
 			var a = $(elementId + " a");
 			a.text(realPage);
 			a.attr("href", "#page" + realPage);
 		}
 	},
-	
+
 	handleChangePageSelection: function(evt) {
 		if ($(evt.currentTarget).hasClass("disabled")) {
 			return;
@@ -250,7 +250,7 @@ azkaban.ExecutionsView = Backbone.View.extend({
 		var page = evt.currentTarget.page;
 		this.model.set({"page": page});
 	},
-	
+
 	handleChangeView: function(evt) {
 		if (this.init) {
 			return;
@@ -259,23 +259,23 @@ azkaban.ExecutionsView = Backbone.View.extend({
 		this.handlePageChange(evt);
 		this.init = true;
 	},
-	
+
 	handlePageChange: function(evt) {
 		var page = this.model.get("page") - 1;
 		var pageSize = this.model.get("pageSize");
 		var requestURL = contextURL + "/manager";
-		
+
 		var model = this.model;
 		var requestData = {
-			"project": projectName, 
-			"flow": flowId, 
-			"ajax": "fetchFlowExecutions", 
-			"start": page * pageSize, 
+			"project": projectName,
+			"flow": flowId,
+			"ajax": "fetchFlowExecutions",
+			"start": page * pageSize,
 			"length": pageSize
 		};
 		var successHandler = function(data) {
 			model.set({
-				"executions": data.executions, 
+				"executions": data.executions,
 				"total": data.total
 			});
 			model.trigger("render");
@@ -289,11 +289,11 @@ azkaban.SummaryView = Backbone.View.extend({
 	events: {
     'click #analyze-btn': 'fetchLastRun'
 	},
-	
+
 	initialize: function(settings) {
 		this.model.bind('change:view', this.handleChangeView, this);
 		this.model.bind('render', this.render, this);
-		
+
 		this.fetchDetails();
 		this.fetchSchedule();
 		this.model.trigger('render');
@@ -306,7 +306,7 @@ azkaban.SummaryView = Backbone.View.extend({
 			'project': projectName,
 			'flow': flowId
 		};
-		
+
 		var model = this.model;
 
 		var successHandler = function(data) {
@@ -318,7 +318,7 @@ azkaban.SummaryView = Backbone.View.extend({
 		};
 		$.get(requestURL, requestData, successHandler, 'json');
 	},
-	
+
   fetchSchedule: function() {
 		var requestURL = contextURL + "/schedule"
 		var requestData = {
@@ -327,9 +327,34 @@ azkaban.SummaryView = Backbone.View.extend({
 			'flowId': flowId
 		};
 		var model = this.model;
+    var view = this;
 		var successHandler = function(data) {
 			model.set({'schedule': data.schedule});
 			model.trigger('render');
+      view.fetchSla();
+		};
+		$.get(requestURL, requestData, successHandler, 'json');
+	},
+
+  fetchSla: function() {
+    var schedule = this.model.get('schedule');
+    if (schedule == null || schedule.scheduleId == null) {
+      return;
+    }
+
+		var requestURL = contextURL + "/schedule"
+		var requestData = {
+			"scheduleId": schedule.scheduleId,
+			"ajax": "slaInfo"
+		};
+		var model = this.model;
+		var successHandler = function(data) {
+      if (data == null || data.settings == null || data.settings.length == 0) {
+        return;
+      }
+      schedule.slaOptions = true;
+      model.set({'schedule': schedule});
+			model.trigger('render');
 		};
 		$.get(requestURL, requestData, successHandler, 'json');
 	},
@@ -390,10 +415,10 @@ $(function() {
 	// Execution model has to be created before the window switches the tabs.
 	executionModel = new azkaban.ExecutionModel();
 	executionsView = new azkaban.ExecutionsView({
-		el: $('#executionsView'), 
+		el: $('#executionsView'),
 		model: executionModel
 	});
-	
+
   summaryModel = new azkaban.SummaryModel();
 	summaryView = new azkaban.SummaryView({
 		el: $('#summaryView'),
@@ -407,35 +432,35 @@ $(function() {
 	});
 
   flowTabView = new azkaban.FlowTabView({
-		el: $('#headertabs'), 
-		selectedView: selected 
+		el: $('#headertabs'),
+		selectedView: selected
 	});
 
 	graphModel = new azkaban.GraphModel();
 	mainSvgGraphView = new azkaban.SvgGraphView({
-		el: $('#svgDiv'), 
-		model: graphModel, 
-		rightClick: { 
-			"node": nodeClickCallback, 
-			"edge": edgeClickCallback, 
-			"graph": graphClickCallback 
+		el: $('#svgDiv'),
+		model: graphModel,
+		rightClick: {
+			"node": nodeClickCallback,
+			"edge": edgeClickCallback,
+			"graph": graphClickCallback
 		}
 	});
-	
+
   jobsListView = new azkaban.JobListView({
-		el: $('#joblist-panel'), 
-		model: graphModel, 
+		el: $('#joblist-panel'),
+		model: graphModel,
 		contextMenuCallback: jobClickCallback
 	});
-	
+
   executionsTimeGraphView = new azkaban.TimeGraphView({
-		el: $('#timeGraph'), 
+		el: $('#timeGraph'),
 		model: executionModel,
     modelField: 'executions'
 	});
-	
+
 	slaView = new azkaban.ChangeSlaView({el:$('#sla-options')});
-	
+
 	var requestURL = contextURL + "/manager";
 	// Set up the Flow options view. Create a new one every time :p
 	$('#executebtn').click(function() {
@@ -451,15 +476,15 @@ $(function() {
 	});
 
 	var requestData = {
-		"project": projectName, 
-		"ajax": "fetchflowgraph", 
+		"project": projectName,
+		"ajax": "fetchflowgraph",
 		"flow": flowId
 	};
 	var successHandler = function(data) {
 		console.log("data fetched");
 		graphModel.addFlow(data);
 		graphModel.trigger("change:graph");
-		
+
 		// Handle the hash changes here so the graph finishes rendering first.
 		if (window.location.hash) {
 			var hash = window.location.hash;
@@ -470,7 +495,7 @@ $(function() {
 				flowTabView.handleSummaryLinkClick();
 			}
 			else if (hash == "#graph") {
-				// Redundant, but we may want to change the default. 
+				// Redundant, but we may want to change the default.
 				selected = "graph";
 			}
 			else {
diff --git a/src/web/js/azkaban/view/flow-stats.js b/src/web/js/azkaban/view/flow-stats.js
index 5f2919e..d991a4c 100644
--- a/src/web/js/azkaban/view/flow-stats.js
+++ b/src/web/js/azkaban/view/flow-stats.js
@@ -1,12 +1,12 @@
 /*
  * Copyright 2012 LinkedIn Corp.
- * 
+ *
  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  * use this file except in compliance with the License. You may obtain a copy of
  * the License at
- * 
+ *
  * http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -21,11 +21,16 @@ azkaban.FlowStatsView = Backbone.View.extend({
   events: {
   },
 
+  histogram: true,
+
 	initialize: function(settings) {
 		this.model.bind('change:view', this.handleChangeView, this);
 		this.model.bind('render', this.render, this);
+    if (settings.histogram != null) {
+      this.histogram = settings.histogram;
+    }
   },
-	
+
   render: function(evt) {
   },
 
@@ -38,10 +43,10 @@ azkaban.FlowStatsView = Backbone.View.extend({
     var requestData = {"execid": execId, "ajax":"fetchexecflow"};
     var jobs = [];
     var successHandler = function(data) {
-      for (var i = 0; i < data.nodes.length; ++i) {
-        var node = data.nodes[i];
-        jobs.push(node.id);
-      }
+      data.nodes.sort(function(a, b) {
+        return a.startTime - b.startTime;
+      });
+      jobs = data.nodes;
     };
     $.ajax({
       url: requestURL,
@@ -127,7 +132,7 @@ azkaban.FlowStatsView = Backbone.View.extend({
         }
       }
       if (str.indexOf('Xms') > -1) {
-        if (str.length <= 4) { 
+        if (str.length <= 4) {
           continue;
         }
         var size = str.substring(4, str.length);
@@ -179,13 +184,13 @@ azkaban.FlowStatsView = Backbone.View.extend({
       stats.fileBytesWritten.max = fileBytesWritten;
       stats.fileBytesWritten.job = job;
     }
-    
+
     var hdfsBytesRead = parseInt(fileSystemCounters['HDFS_BYTES_READ']);
     if (hdfsBytesRead >= stats.hdfsBytesRead.max) {
       stats.hdfsBytesRead.max = hdfsBytesRead;
       stats.hdfsBytesRead.job = job;
     }
-    
+
     var hdfsBytesWritten = parseInt(fileSystemCounters['HDFS_BYTES_WRITTEN']);
     if (hdfsBytesWritten >= stats.hdfsBytesWritten.max) {
       stats.hdfsBytesWritten.max = hdfsBytesWritten;
@@ -219,6 +224,8 @@ azkaban.FlowStatsView = Backbone.View.extend({
       success: false,
       message: null,
       warnings: [],
+      durations: [],
+      histogram: this.histogram,
       stats: {
         mapSlots: {
           max: 0,
@@ -275,18 +282,36 @@ azkaban.FlowStatsView = Backbone.View.extend({
       }
     };
 
+    var jobsAnalyzed = 0;
     for (var i = 0; i < jobs.length; ++i) {
       var job = jobs[i];
-      var jobStats = this.fetchJobStats(job, execId);
+      var duration = job.endTime - job.startTime;
+      data.durations.push({
+        job: job.id,
+        duration: duration
+      });
+
+      var jobStats = this.fetchJobStats(job.id, execId);
       if (jobStats.jobStats == null) {
         data.warnings.push("No job stats available for job " + job.id);
         continue;
       }
       for (var j = 0; j < jobStats.jobStats.length; ++j) {
-        this.updateStats(jobStats.jobStats[j], data, job);
+        this.updateStats(jobStats.jobStats[j], data, job.id);
       }
+      ++jobsAnalyzed;
+    }
+
+    // If no jobs were analyzed, then no jobs had any job stats available. In
+    // this case, display a No Flow Stats Available message.
+    if (jobsAnalyzed == 0) {
+      data.success = false;
+      data.message = "There were no job stats provided by any job.";
+    }
+    else {
+      this.finalizeStats(data);
     }
-    this.finalizeStats(data);
+
     this.model.set({'data': data});
     this.model.trigger('render');
   },
@@ -300,14 +325,30 @@ azkaban.FlowStatsView = Backbone.View.extend({
         view.display(out);
       });
     }
-    else if (data.success == "false") {
+    else if (data.success == false) {
       dust.render("flowstats-no-data", data, function(err, out) {
         view.display(out);
       });
     }
     else {
+      var histogram = this.histogram;
       dust.render("flowstats", data, function(err, out) {
         view.display(out);
+        if (histogram == true) {
+          var yLabelFormatCallback = function(y) {
+            var seconds = y / 1000.0;
+            return seconds.toString() + " s";
+          };
+
+          Morris.Bar({
+            element: "job-histogram",
+            data: data.durations,
+            xkey: "job",
+            ykeys: ["duration"],
+            labels: ["Duration"],
+            yLabelFormat: yLabelFormatCallback
+          });
+        }
       });
     }
   },