azkaban-developers

Changes

ivy.xml 2(+0 -2)

src/less/base.less 21(+20 -1)

src/less/flow.less 116(+72 -44)

src/less/job.less 8(+0 -8)

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

src/web/js/azkaban/model/log-data.js 346(+0 -346)

Details

ivy.xml 2(+0 -2)

diff --git a/ivy.xml b/ivy.xml
index 4e85d54..100d96b 100644
--- a/ivy.xml
+++ b/ivy.xml
@@ -33,8 +33,6 @@
         rev="${guava.version}" conf="default->master" />
     <dependency name="h2" org="com.h2database" 
         rev="${h2.version}" conf="default->master" />
-    <dependency name="hadoop-core" org="org.apache.hadoop" 
-        rev="${hadoop-core.version}" conf="default->master" />
     <dependency name="httpclient" org="org.apache.httpcomponents" 
         rev="${httpclient.version}" conf="default->master" />
     <dependency name="httpcore" org="org.apache.httpcomponents" 
diff --git a/ivy/libraries.properties b/ivy/libraries.properties
index cb1bf47..4beee65 100644
--- a/ivy/libraries.properties
+++ b/ivy/libraries.properties
@@ -13,7 +13,6 @@ commons-logging.version=1.1.1
 commons-pool.version=1.6
 guava.version=13.0.1
 h2.version=1.3.170
-hadoop-core.version=1.0.4
 httpclient.version=4.2.1
 httpcore.version=4.2.1
 jackson-core.version=1.9.5
diff --git a/src/java/azkaban/flow/Edge.java b/src/java/azkaban/flow/Edge.java
index 5820a33..14fc6a0 100644
--- a/src/java/azkaban/flow/Edge.java
+++ b/src/java/azkaban/flow/Edge.java
@@ -39,9 +39,9 @@ public class Edge {
 	}
 
 	public Edge(Edge clone) {
-		this.sourceId = clone.sourceId;
-		this.targetId = clone.targetId;
-		this.error = clone.error;
+		this.sourceId = clone.getSourceId();
+		this.targetId = clone.getTargetId();
+		this.error = clone.getError();
 	}
 	
 	public String getId() {
@@ -101,7 +101,7 @@ public class Edge {
 		HashMap<String, Object> obj = new HashMap<String, Object>();
 		obj.put("source", getSourceId());
 		obj.put("target", getTargetId());
-		if (error != null) {
+		if (hasError()) {
 			obj.put("error", error);
 		}
 		if (guideValues != null) {
diff --git a/src/java/azkaban/project/Project.java b/src/java/azkaban/project/Project.java
index 76bf207..e3e0cde 100644
--- a/src/java/azkaban/project/Project.java
+++ b/src/java/azkaban/project/Project.java
@@ -265,17 +265,6 @@ public class Project {
 			projectObject.put("metadata", metadata);
 		}
 
-		ArrayList<Map<String, Object>> users = new ArrayList<Map<String, Object>>();
-		for (Map.Entry<String, Permission> entry : userPermissionMap.entrySet()) {
-			HashMap<String, Object> userMap = new HashMap<String, Object>();
-			userMap.put("userId", entry.getKey());
-			userMap.put("permissions", entry.getValue().toStringArray());
-			users.add(userMap);
-		}
-		
-		projectObject.put("users", users);
-		
-
 		ArrayList<String> proxyUserList = new ArrayList<String>(proxyUsers);
 		projectObject.put("proxyUsers", proxyUserList);
 		
@@ -312,18 +301,6 @@ public class Project {
 			project.setMetadata(metadata);
 		}
 		
-		List<Map<String, Object>> users = (List<Map<String, Object>>) projectObject
-				.get("users");
-
-		for (Map<String, Object> user : users) {
-			String userid = (String) user.get("userId");
-			Permission perm = new Permission();
-			List<String> list = (List<String>) user.get("permissions");
-			perm.addPermissionsByName(list);
-
-			project.setUserPermission(userid, perm);
-		}
-		
 		List<String> proxyUserList = (List<String>) projectObject.get("proxyUsers");
 		project.addAllProxyUsers(proxyUserList);
 
diff --git a/src/java/azkaban/webapp/AzkabanWebServer.java b/src/java/azkaban/webapp/AzkabanWebServer.java
index 2133db8..ebb0aa0 100644
--- a/src/java/azkaban/webapp/AzkabanWebServer.java
+++ b/src/java/azkaban/webapp/AzkabanWebServer.java
@@ -999,7 +999,7 @@ public class AzkabanWebServer extends AzkabanServer {
 			
 			String pluginName = pluginProps.getString("viewer.name");
 			String pluginWebPath = pluginProps.getString("viewer.path");
-			String pluginJobType = pluginProps.getString("viewer.jobtype", null);
+			String pluginJobTypes = pluginProps.getString("viewer.jobtypes", null);
 			int pluginOrder = pluginProps.getInt("viewer.order", 0);
 			boolean pluginHidden = pluginProps.getBoolean("viewer.hidden", false);
 			List<String> extLibClasspath = pluginProps.getStringList("viewer.external.classpaths", (List<String>)null);
@@ -1108,7 +1108,7 @@ public class AzkabanWebServer extends AzkabanServer {
 						pluginWebPath, 
 						pluginOrder, 
 						pluginHidden,
-						pluginJobType));
+						pluginJobTypes));
 		}
 		
 		// Velocity needs the jar resource paths to be set.
diff --git a/src/java/azkaban/webapp/plugin/PluginRegistry.java b/src/java/azkaban/webapp/plugin/PluginRegistry.java
index b9382b6..a6f74d2 100644
--- a/src/java/azkaban/webapp/plugin/PluginRegistry.java
+++ b/src/java/azkaban/webapp/plugin/PluginRegistry.java
@@ -24,34 +24,37 @@ import java.util.TreeSet;
 
 public class PluginRegistry {
 
-  private static PluginRegistry registry;
+	private static PluginRegistry registry;
 
-  public TreeSet<ViewerPlugin> viewerPlugins;
+	public TreeSet<ViewerPlugin> viewerPlugins;
 
 	public Map<String, TreeSet<ViewerPlugin>> jobTypeViewerPlugins;
 
-  private PluginRegistry() {
+	private PluginRegistry() {
 		viewerPlugins = new TreeSet<ViewerPlugin>(ViewerPlugin.COMPARATOR);
 		jobTypeViewerPlugins = new HashMap<String, TreeSet<ViewerPlugin>>();
-  }
+	}
 
-  public void register(ViewerPlugin plugin) {
+	public void register(ViewerPlugin plugin) {
 		viewerPlugins.add(plugin);
-		String jobType = plugin.getJobType();
-		if (jobType == null) {
+		List<String> jobTypes = plugin.getJobTypes();
+		if (jobTypes == null) {
 			return;
 		}
-		TreeSet<ViewerPlugin> plugins = null;
-		if (!jobTypeViewerPlugins.containsKey(jobType)) {
-			plugins = new TreeSet<ViewerPlugin>(ViewerPlugin.COMPARATOR);
-			plugins.add(plugin);
-			jobTypeViewerPlugins.put(jobType, plugins);
-		}
-		else {
-			plugins = jobTypeViewerPlugins.get(jobType);
-			plugins.add(plugin);
+
+		for (String jobType : jobTypes) {
+			TreeSet<ViewerPlugin> plugins = null;
+			if (!jobTypeViewerPlugins.containsKey(jobType)) {
+				plugins = new TreeSet<ViewerPlugin>(ViewerPlugin.COMPARATOR);
+				plugins.add(plugin);
+				jobTypeViewerPlugins.put(jobType, plugins);
+			}
+			else {
+				plugins = jobTypeViewerPlugins.get(jobType);
+				plugins.add(plugin);
+			}
 		}
-  }
+	}
 
 	public List<ViewerPlugin> getViewerPlugins() {
 		return new ArrayList<ViewerPlugin>(viewerPlugins);
@@ -65,10 +68,10 @@ public class PluginRegistry {
 		return new ArrayList<ViewerPlugin>(plugins);
 	}
 
-  public static PluginRegistry getRegistry() {
-    if (registry == null) {
-      registry = new PluginRegistry();
-    }
-    return registry;
-  }
+	public static PluginRegistry getRegistry() {
+		if (registry == null) {
+			registry = new PluginRegistry();
+		}
+		return registry;
+	}
 }
diff --git a/src/java/azkaban/webapp/plugin/ViewerPlugin.java b/src/java/azkaban/webapp/plugin/ViewerPlugin.java
index 9e167ff..b2bb851 100644
--- a/src/java/azkaban/webapp/plugin/ViewerPlugin.java
+++ b/src/java/azkaban/webapp/plugin/ViewerPlugin.java
@@ -16,14 +16,16 @@
 
 package azkaban.webapp.plugin;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Comparator;
 
 public class ViewerPlugin {
 	private final String pluginName;
 	private final String pluginPath;
-	private final String jobType;
 	private final int order;
 	private boolean hidden;
+	private final List<String> jobTypes;
 
 	public static final Comparator<ViewerPlugin> COMPARATOR = 
 			new Comparator<ViewerPlugin>() {
@@ -41,12 +43,12 @@ public class ViewerPlugin {
 			String pluginPath, 
 			int order, 
 			boolean hidden,
-			String jobType) {
+			String jobTypes) {
 		this.pluginName = pluginName;
 		this.pluginPath = pluginPath;
 		this.order = order;
 		this.setHidden(hidden);
-		this.jobType = jobType;
+		this.jobTypes = parseJobTypes(jobTypes);
 	}
 
 	public String getPluginName() {
@@ -69,7 +71,19 @@ public class ViewerPlugin {
 		this.hidden = hidden;
 	}
 
-	public String getJobType() {
-		return jobType;
+	protected List<String> parseJobTypes(String jobTypesStr) {
+		if (jobTypesStr == null) {
+			return null;
+		}
+		String[] parts = jobTypesStr.split(",");
+		List<String> jobTypes = new ArrayList<String>();
+		for (int i = 0; i < parts.length; ++i) {
+			jobTypes.add(parts[i].trim());
+		}
+		return jobTypes;
+	}
+
+	public List<String> getJobTypes() {
+		return jobTypes;
 	}
 }
diff --git a/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm b/src/java/azkaban/webapp/servlet/velocity/executingflowpage.vm
index 149ca0e..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">
@@ -170,7 +172,7 @@
       <div id="flow-stats-container">
         <div class="row">
           <div class="col-lg-12">
-            <div class="alert alert-default">
+            <div class="callout callout-default">
               <h4>No stats available</h4>
               <p>Stats for this flow execution are not available.</p>
             </div>
@@ -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/flowgraphview.vm b/src/java/azkaban/webapp/servlet/velocity/flowgraphview.vm
index fdc224f..04b71a4 100644
--- a/src/java/azkaban/webapp/servlet/velocity/flowgraphview.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/flowgraphview.vm
@@ -15,17 +15,20 @@
 *#
 
 	## Graph view.
+
 		<div class="container-full container-fill" id="graphView">
-			<div class="glyphicon glyphicon-th-list" id="open-joblist-btn" title="Open Job List Panel"></div>
-			<div class="graph-sidebar">
+			<div class="graph-sidebar-open" id="open-joblist-btn">
+				<span class="glyphicon glyphicon-th-list" title="Open Job List Panel"></span>
+			</div>
+			<div class="graph-sidebar-float">
 				<div class="panel panel-default" id="joblist-panel">
 					<div class="panel-heading">
-						<div id="close-btn" title="Close Panel"><span class="glyphicon glyphicon-remove"></span></div>
-						<div id="inputbox-panel">
+						<div class="graph-sidebar-close" id="close-btn" title="Close Panel"><span class="glyphicon glyphicon-remove"></span></div>
+						<div class="graph-sidebar-search">
 							<input id="filter" type="text" placeholder="Job Filter" class="form-control input-sm">
 						</div>
 					</div>
-					<div id="joblist"></div>
+					<div id="joblist" class="graph-sidebar-list"></div>
 					<div class="panel-footer">
 						<button type="button" class="btn btn-sm btn-default" id="resetPanZoomBtn">Reset Pan Zoom</button>
 						<button type="button" class="btn btn-sm btn-default" id="autoPanZoomBtn" data-toggle="button">Auto Pan Zoom</button>
@@ -33,9 +36,9 @@
 				</div><!-- /.panel -->
 			</div>
 			<div class="col-content">
-				<div id="svgDiv" class="well well-clear well-sm">
+				<div id="svgDiv" class="well well-clear well-sm graph-container">
 					<svg id="flow-graph" xmlns="http://www.w3.org/2000/svg" version="1.1" shape-rendering="optimize-speed" text-rendering="optimize-speed">
 					</svg>
 				</div>
 			</div>
-		</div>
\ No newline at end of file
+		</div>
diff --git a/src/java/azkaban/webapp/servlet/velocity/flowpage.vm b/src/java/azkaban/webapp/servlet/velocity/flowpage.vm
index 48039cd..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")
@@ -147,9 +147,9 @@
       <div id="flow-stats-container">
         <div class="row">
           <div class="col-xs-12">
-            <div class="alert alert-info">
+            <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/index.vm b/src/java/azkaban/webapp/servlet/velocity/index.vm
index 3d020b3..a4fc481 100644
--- a/src/java/azkaban/webapp/servlet/velocity/index.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/index.vm
@@ -110,7 +110,7 @@
   #end
           </ul>
 #else
-          <div class="alert alert-default">
+          <div class="callout callout-default">
             <h4>No Viewable Projects</h4>
             <p>Click Create Project to create a new project.</p>
           </div>
diff --git a/src/java/azkaban/webapp/servlet/velocity/jobdetailsheader.vm b/src/java/azkaban/webapp/servlet/velocity/jobdetailsheader.vm
index 27863ea..8d11704 100644
--- a/src/java/azkaban/webapp/servlet/velocity/jobdetailsheader.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/jobdetailsheader.vm
@@ -49,11 +49,9 @@
 
 			<ul class="nav nav-tabs nav-sm" id="headertabs">
 	#if ($current_page == "executing")
-				<li id="jobLogViewLink"><a href="#logs">Job Logs</a></li>
-				<li id="jobSummaryViewLink"><a href="#summary">Summary</a></li>
+				<li class="active" id="jobLogViewLink"><a href="#logs">Job Logs</a></li>
 	#else
 				<li id="jobLogViewLink"><a href="${context}/executor?execid=${execid}&job=${jobid}#logs">Job Logs</a></li>
-				<li id="jobSummaryViewLink"><a href="${context}/executor?execid=${execid}&job=${jobid}#summary">Summary</a></li>
 	#end
 	#foreach ($jobViewerPlugin in $jobViewerPlugins)
 				<li#if($current_page == $jobViewerPlugin.pluginName) class="active"#end><a href="$!context/${jobViewerPlugin.pluginPath}?execid=${execid}&jobid=${jobid}">$jobViewerPlugin.pluginName</a></li>
diff --git a/src/java/azkaban/webapp/servlet/velocity/jobdetailspage.vm b/src/java/azkaban/webapp/servlet/velocity/jobdetailspage.vm
index 27f5d40..80d90e1 100644
--- a/src/java/azkaban/webapp/servlet/velocity/jobdetailspage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/jobdetailspage.vm
@@ -22,7 +22,7 @@
 #parse("azkaban/webapp/servlet/velocity/javascript.vm")
 
 		<script type="text/javascript" src="${context}/js/azkaban/util/ajax.js"></script>
-		<script type="text/javascript" src="${context}/js/azkaban/model/log-data.js"></script>
+		<script type="text/javascript" src="${context}/js/azkaban/model/job-log.js"></script>
 		<script type="text/javascript" src="${context}/js/azkaban/view/job-details.js"></script>
 		<script type="text/javascript">
 			var contextURL = "${context}";
@@ -68,75 +68,10 @@
             </div>
           </div>
         </div>
-			</div>
-    </div>
-
-	## Job Summary
-
-    <div class="container-full" id="jobSummaryView">
-      <div class="row">
-        <div class="col-lg-12">
-          <h3>
-            Job Summary
-            <div class="pull-right">
-              <button type="button" id="updateSummaryBtn" class="btn btn-xs btn-default">Refresh</button>
-            </div>
-          </h3>
-
-          <div id="jobType">
-            <table id="jobTypeTable" class="table table-striped table-bordered table-hover">
-            </table>
-          </div>
-
-          <div id="command-summary">
-            <h4>Command Summary</h4>
-            <table id="commandTable" class="table table-striped table-bordered table-hover">
-            </table>
-          </div>
-        
-          <div id="pigJobSummary">
-            <h4>Pig Job Summary</h4>
-            <table class="table table-striped table-bordered table-hover">
-              <thead id="summaryHeader">
-              </thead>
-              <tbody id="summaryBody">
-              </tbody>
-            </table>
-          </div>
-        
-          <div id="pigJobStats">
-            <h4>Pig Job Stats</h4>
-              <div class="panel-body-stats">
-                <table class="table table-striped table-bordered table-hover table-condensed">
-                  <thead id="statsHeader">
-                  </thead>
-                  <tbody id="statsBody">
-                  </tbody>
-                </table>
-            </div>
-          </div>
-
-          <div id="hiveJobSummary">
-            <h4>Hive Job Summary</h4>
-            <table class="table table-striped table-bordered table-hover" id="hiveTable">
-              <thead id="hiveTableHeader">
-              </thead>
-              <tbody id="hiveTableBody">
-              </tbody>
-            </table>
-          </div>
-
-          <div id="jobIds">
-            <h4>Map Reduce Jobs</h4>
-            <table class="table table-striped table-bordered table-hover">
-              <tbody id="jobIdsTableBody"></tbody>
-            </table>
-          </div>
-        </div>
       </div>
     </div>
-			
-	## Error message message dialog.
+	
+    ## Error message message dialog.
 
     <div class="container-full">
 			<div class="modal" id="messageDialog">
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>
diff --git a/src/java/azkaban/webapp/servlet/velocity/permissionspage.vm b/src/java/azkaban/webapp/servlet/velocity/permissionspage.vm
index 067b89c..5b424de 100644
--- a/src/java/azkaban/webapp/servlet/velocity/permissionspage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/permissionspage.vm
@@ -249,7 +249,7 @@
 						</div>
 						<div class="modal-body">
 							<div class="alert alert-danger" id="remove-proxy-error-msg"></div>
-              <p><strong>Warning:</strong> Removing Proxy User</p>
+              <p><strong>Warning:</strong> <span id="remove-proxy-msg"></span></p>
 						</div>
 						<div class="modal-footer">
 							<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
@@ -294,10 +294,10 @@
 					<div class="modal-content">
 						<div class="modal-header">
 							<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-							<h4 class="modal-title">Change Permissions</h4>
+							<h4 class="modal-title" id="change-title">Change Permissions</h4>
 						</div>
 						<div class="modal-body">
-							<div class="alert alert-danger" id="change-permission-error-msg"></div>
+              <div class="alert alert-danger" id="change-permission-error-msg"></div>
 							<fieldset class="form-horizontal">
 								<div class="form-group">
 									<label for="path" class="col-sm-2 control-label">User</label>
diff --git a/src/java/azkaban/webapp/servlet/velocity/projectpage.vm b/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
index 52d4e37..50e2606 100644
--- a/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/projectpage.vm
@@ -85,7 +85,7 @@
             </div>
 		#end
 	#else
-            <div class="alert alert-default">
+            <div class="callout callout-default">
               <h4>No Flows</h4>
               <p>No flows have been uploaded to this project yet.</p>
             </div>
diff --git a/src/less/azkaban.less b/src/less/azkaban.less
index 54ba380..0f13144 100644
--- a/src/less/azkaban.less
+++ b/src/less/azkaban.less
@@ -1,3 +1,4 @@
+@import "variables.less";
 @import "non-responsive.less";
 
 @import "base.less";
@@ -8,9 +9,9 @@
 
 @import "context-menu.less";
 @import "tables.less";
+@import "callout.less";
 
 @import "login.less";
 @import "project.less";
 @import "flow.less";
-@import "job.less";
 @import "log.less";

src/less/base.less 21(+20 -1)

diff --git a/src/less/base.less b/src/less/base.less
index 7b8f11e..4c7445d 100644
--- a/src/less/base.less
+++ b/src/less/base.less
@@ -115,7 +115,7 @@
     background-position: -64px -80px;
   }
 }
-  
+
 .editable {
   margin: 0px;
   cursor: pointer;
@@ -135,3 +135,22 @@
   padding: 8px 12px;
   font-size: 13px;
 }
+
+.scrollable {
+  padding: 0;
+  overflow: auto;
+  margin-bottom: 20px;
+
+  table {
+    margin-bottom: 0;
+  }
+}
+
+.panel-scrollable {
+  padding: 0;
+  overflow: auto;
+
+  table {
+    margin-bottom: 0;
+  }
+}
diff --git a/src/less/callout.less b/src/less/callout.less
new file mode 100644
index 0000000..cdf891d
--- /dev/null
+++ b/src/less/callout.less
@@ -0,0 +1,64 @@
+/*
+ * Callouts
+ *
+ * Not quite alerts, but custom and helpful notes for folks reading the docs.
+ * Requires a base and modifier class.
+ */
+
+/* Common styles for all types */
+.callout {
+  margin: 0 0 20px 0;
+  padding: 20px;
+  border-left: 3px solid #eee;
+
+  h4 {
+    margin-top: 0;
+    margin-bottom: 5px;
+  }
+
+  p:last-child {
+    margin-bottom: 0;
+  }
+
+  code {
+    background-color: #fff;
+    border-radius: 3px;
+  }
+}
+
+/* Variations */
+.callout-danger {
+  background-color: #fdf7f7;
+  border-color: #d9534f;
+  
+  h4 {
+    color: #d9534f;
+  }
+}
+
+.callout-warning {
+  background-color: #fcf8f2;
+  border-color: #f0ad4e;
+
+  h4 {
+    color: #f0ad4e;
+  }
+}
+
+.callout-info {
+  background-color: #f4f8fa;
+  border-color: #5bc0de;
+
+  h4 {
+    color: #5bc0de;
+  }
+}
+
+.callout-default {  
+  background-color: #f5f5f5;
+  border-color: #dddddd;
+
+  h4 {
+    color: #a0a0a0;
+  }
+}

src/less/flow.less 116(+72 -44)

diff --git a/src/less/flow.less b/src/less/flow.less
index 7b123a6..8ee150a 100644
--- a/src/less/flow.less
+++ b/src/less/flow.less
@@ -1,6 +1,6 @@
 #svgDiv {
-  height: 100%;
-  padding: 0px;
+    height: 100%;
+    padding: 0px;
 }
 
 #graphView {
@@ -11,8 +11,8 @@
 }
 
 #flow-graph {
-  width: 100%;
-  height: 100%;
+    width: 100%;
+    height: 100%;
 }
 
 #headertabs {
@@ -54,19 +54,19 @@
   }
 
   &.SUCCEEDED {
-    background-color: #5cb85c;
+    background-color: @flow-succeeded-color;
   }
 
   &.FAILED {
-    background-color: #d9534f;
+    background-color: @flow-failed-color;
   }
   
   &.KILLED {
-    background-color: #d9534f;
+    background-color: @flow-killed-color;
   }
 
   &.RUNNING {
-    background-color: #3398cc;	
+    background-color: @flow-running-color;
     background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));
     background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
     background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
@@ -77,11 +77,11 @@
   }
 
   &.QUEUED {
-    background-color: #009fc9;
+    background-color: @flow-queued-color;
   }
   
   &.CANCELLED {
-    background-color: #ff9999;
+    background-color: @flow-cancelled-color;
   }
 }
 
@@ -104,82 +104,123 @@ td {
     margin-top: 2px;
     
     &.SUCCEEDED {
-      background-color: #5cb85c;
+      background-color: @flow-succeeded-color;
     }
 
     &.FAILED {
-      background-color: #d9534f;
+      background-color: @flow-failed-color;
     }
     
     &.KILLED {
-      background-color: #d9534f;
+      background-color: @flow-killed-color;
     }
 
     &.PAUSED {
-      background-color: #c82123;
+      background-color: @flow-paused-color;
     }
 
     &.READY,
-    &.UNKNOWN {
-      background-color: #ccc;
+    &.UNKNOWN,
+    &.PREPARING {
+      background-color: @flow-default-color;
     }
 
     &.RUNNING {
-      background-color: #3398cc;	
+      background-color: @flow-running-color;
     }
 
     &.FAILED_FINISHING {
-      background-color: #f19153;	
+      background-color: @flow-failed-finishing-color;
     }
 
     &.DISABLED,
     &.SKIPPED {
-      background-color: #aaa;	
+      background-color: @flow-disabled-color;
     }
 
     &.CANCELLED {
-      background-color: #ff9999;
+      background-color: @flow-cancelled-color;
     }
   }
 }
 
 #flowStatus {
   &.SKIPPED {
-    color: #aaa;
+    color: @flow-disabled-color;
   }
 
   &.SUCCEEDED {
-    color: #4e911e;
+    color: @flow-succeeded-color;
   }
 
   &.RUNNING {
-    color: #009fc9;
+    color: @flow-running-color;
   }
 
   &.PAUSED {
-    color: #c92123;
+    color: @flow-paused-color;
+  }
+
+  &.FAILED {
+    color: @flow-failed-color;
+  }
+
+  &.KILLED {
+    color: @flow-killed-color;
   }
 
-  &.FAILED,
-  &.FAILED_FINISHING,
-  &.KILLED,
   &.CANCELLED {
-    color: #cc0000;
+    color: @flow-cancelled-color;
+  }
+
+  &.FAILED_FINISHING {
+    color: @flow-failed-finishing-color;
   }
 }
 
 .graph-sidebar {
+  height: 100%;
+  overflow-y: auto;
+
+  .graph-sidebar-list {
+    height: 100%;
+  }
+}
+
+.graph-sidebar-float {
   position: absolute;
   top: 0px;
   bottom: 0px;
+
+  .graph-sidebar-list {
+    overflow-y: auto;
+    height: calc(~"100% - 102px");
+  }
+
+  .panel {
+    height: 100%;
+    
+    .panel-heading {
+      padding-right: 10px;
+    }
+  }
 }
 
-#inputbox-panel {
+.graph-container {
+  height: 100%;
+
+  svg {
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.graph-sidebar-search {
 	width: 206px;
 	margin: 0px;
 }
 
-#close-btn {
+.graph-sidebar-close {
 	float: right;
 	color: #CCC;
 	padding: 5px 0px;
@@ -190,15 +231,7 @@ td {
 	}
 }
 
-#joblist-panel {
-	height: 100%;
-	
-	.panel-heading {
-		padding-right: 10px;
-	}
-}
-
-#open-joblist-btn {
+.graph-sidebar-open {
 	position: absolute;
 	margin: 10px;
 	color: #CCC;
@@ -209,11 +242,6 @@ td {
 	}
 }
 
-// TODO: Rename this as #job-list
-#joblist {
-  overflow-y: auto;
-  height: calc(~"100% - 102px");
-}
 ul.tree-list {
   list-style-type: none;
   padding-left: 0px;
diff --git a/src/less/Makefile b/src/less/Makefile
index 052a96c..37180b3 100644
--- a/src/less/Makefile
+++ b/src/less/Makefile
@@ -8,18 +8,19 @@ all: $(OBJ)
 
 azkaban_css_DEPS = \
 	azkaban.less \
+	variables.less \
 	base.less \
 	context-menu.less \
 	flow.less \
 	header.less \
-	job.less \
 	login.less \
 	log.less \
 	navbar.less \
 	non-responsive.less \
 	off-canvas.less \
 	project.less \
-	tables.less
+	tables.less \
+	callout.less
 
 $(OBJ_DIR)/azkaban.css: $(azkaban_css_DEPS)
 	$(LESSC) $< $@
diff --git a/src/less/variables.less b/src/less/variables.less
new file mode 100644
index 0000000..078d2b2
--- /dev/null
+++ b/src/less/variables.less
@@ -0,0 +1,12 @@
+
+// Flow colors
+@flow-succeeded-color: #5cb85c;
+@flow-failed-color: #d9534f;
+@flow-killed-color: #d9534f;
+@flow-paused-color: #c82123;
+@flow-running-color: #3398cc;
+@flow-failed-finishing-color: #f19153;
+@flow-cancelled-color: #ff9999;
+@flow-queued-color: #009fc9;
+@flow-disabled-color: #aaa;
+@flow-default-color: #ccc;

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/flowstats-no-data.tl b/src/tl/flowstats-no-data.tl
index 2f3605a..fe74240 100644
--- a/src/tl/flowstats-no-data.tl
+++ b/src/tl/flowstats-no-data.tl
@@ -1,6 +1,6 @@
       <div class="row">
         <div class="col-xs-12">
-          <div class="alert alert-default">
+          <div class="callout callout-default">
             <h4>No Flow Stats Available</h4>
             <p>{message}</p>
           </div>
diff --git a/src/tl/flowsummary.tl b/src/tl/flowsummary.tl
index 30c50de..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,19 +46,19 @@
                 <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>
             </tbody>
           </table>
           {:else}
-            <div class="alert alert-default">
+            <div class="callout callout-default">
               <h4>None</h4>
               <p>This flow has not been scheduled.</p>
             </div>
diff --git a/src/web/js/azkaban/model/job-log.js b/src/web/js/azkaban/model/job-log.js
new file mode 100644
index 0000000..d295578
--- /dev/null
+++ b/src/web/js/azkaban/model/job-log.js
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014 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
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+$.namespace('azkaban');
+
+azkaban.JobLogModel = Backbone.Model.extend({
+  initialize: function() {
+    this.set("offset", 0);
+    this.set("logData", "");
+  },
+
+  refresh: function() {
+    var requestURL = contextURL + "/executor"; 
+    var finished = false;
+
+    var date = new Date();
+    var startTime = date.getTime();
+    
+    while (!finished) {
+      var requestData = {
+        "execid": execId,
+        "jobId": jobId,
+        "ajax":"fetchExecJobLogs",
+        "offset": this.get("offset"),
+        "length": 50000,
+        "attempt": attempt
+      };
+
+      var self = this;
+
+      var successHandler = function(data) {
+        console.log("fetchLogs");
+        if (data.error) {
+          console.log(data.error);
+          finished = true;
+        }
+        else if (data.length == 0) {
+          finished = true;
+        }
+        else {
+          var date = new Date();
+          var endTime = date.getTime();
+          if ((endTime - startTime) > 10000) {
+            finished = true;
+            showDialog("Alert", "The log is taking a long time to finish loading. Azkaban has stopped loading them. Please click Refresh to restart the load.");
+          }
+
+          self.set("offset", data.offset + data.length);
+          self.set("logData", self.get("logData") + data.data);
+        }
+      }
+
+      $.ajax({
+        url: requestURL,
+        type: "get",
+        async: false,
+        data: requestData,
+        dataType: "json",
+        error: function(data) {
+          console.log(data);
+          finished = true;
+        },
+        success: successHandler
+      });
+    }
+  },
+});
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
+          });
+        }
       });
     }
   },
diff --git a/src/web/js/azkaban/view/job-details.js b/src/web/js/azkaban/view/job-details.js
index 07b823e..7620b5a 100644
--- a/src/web/js/azkaban/view/job-details.js
+++ b/src/web/js/azkaban/view/job-details.js
@@ -38,247 +38,6 @@ azkaban.JobLogView = Backbone.View.extend({
 	}
 });
 
-var jobSummaryView;
-azkaban.JobSummaryView = Backbone.View.extend({
-	events: {
-		"click #updateSummaryBtn" : "refresh"
-	},
-
-	initialize: function(settings) {
-		$("#jobType").hide();
-		$("#commandSummary").hide();
-		$("#pigJobSummary").hide();
-		$("#pigJobStats").hide();
-		$("#hiveJobSummary").hide();
-		$("#jobIds").hide();
-
-		this.listenTo(this.model, "change:jobType", this.renderJobTypeTable);
-		this.listenTo(this.model, "change:commandProperties", this.renderCommandTable);
-		this.listenTo(this.model, "change:pigSummary", this.renderPigSummaryTable);
-		this.listenTo(this.model, "change:pigStats", this.renderPigStatsTable);
-		this.listenTo(this.model, "change:hiveSummary", this.renderHiveTable);
-		this.listenTo(this.model, "change:jobIds", this.renderJobIdsTable);
-	},
-
-	refresh: function() {
-		this.model.refresh();
-	},
-
-	handleUpdate: function(evt) {
-		renderJobTable(jobSummary.summaryTableHeaders, jobSummary.summaryTableData, "summary");
-		renderJobTable(jobSummary.statTableHeaders, jobSummary.statTableData, "stats");
-		renderHiveTable(jobSummary.hiveQueries, jobSummary.hiveQueryJobs);
-	},
-
-	renderJobTypeTable: function() {
-		var jobTypeTable = $("#jobTypeTable");
-		var jobType = this.model.get("jobType");
-
-		var tr = document.createElement("tr");
-		var td = document.createElement("td");
-		$(td).html("<b>Job Type</b>");
-		$(tr).append(td);
-		td = document.createElement("td");
-		$(td).html(jobType);
-		$(tr).append(td);
-
-		jobTypeTable.append(tr);
-
-		$("#jobType").show();
-	},
-
-	renderJobIdsTable: function() {
-		var oldBody = $("#jobIdsTableBody");
-		var newBody = $(document.createElement("tbody")).attr("id", "jobIdsTableBody");
-
-		var jobIds = this.model.get("jobIds");
-		var jobUrls = this.model.get("jobTrackerUrls");
-		var numJobs = jobIds.length;
-		for (var i = 0; i < numJobs; i++) {
-			var job = jobIds[i];
-			var tr = document.createElement("tr");
-			var td = document.createElement("td");
-			var html = jobUrls[job] ? "<a href='" + jobUrls[job] + "'>" + job + "</a>" : job;
-			$(td).html(html);
-			$(tr).append(td);
-			newBody.append(tr);
-		}
-
-		oldBody.replaceWith(newBody);
-
-		$("#jobIds").show();
-	},
-
-	renderCommandTable: function() {
-		var commandTable = $("#commandTable");
-		var commandProperties = this.model.get("commandProperties");
-
-		for (var key in commandProperties) {
-			if (commandProperties.hasOwnProperty(key)) {
-				var value = commandProperties[key];
-				if (Array.isArray(value)) {
-					value = value.join("<br/>");
-				}
-				var tr = document.createElement("tr");
-				var keyTd = document.createElement("td");
-				var valueTd = document.createElement("td");
-				$(keyTd).html("<b>" + key + "</b>");
-				$(valueTd).html(value);
-				$(tr).append(keyTd);
-				$(tr).append(valueTd);
-				commandTable.append(tr);
-			}
-		}
-
-		$("#commandSummary").show();
-	},
-	renderPigTable: function(tableName, data) {
-		// Add table headers
-		var header = $("#" + tableName + "Header");
-		var tr = document.createElement("tr");
-		var i;
-		var headers = data[0];
-		var numColumns = headers.length;
-		for (i = 0; i < numColumns; i++) {
-			var th = document.createElement("th");
-			$(th).text(headers[i]);
-			$(tr).append(th);
-		}
-		header.append(tr);
-		
-		// Add table body
-		var body = $("#" + tableName + "Body");
-		for (i = 1; i < data.length; i++) {
-			tr = document.createElement("tr");
-			var row = data[i];
-			for (var j = 0; j < numColumns; j++) {
-				var td = document.createElement("td");
-				if (j == 0) {
-					// first column is a link to job details page 
-					$(td).html(row[j]);
-				} else {
-					$(td).text(row[j]);
-				}
-				$(tr).append(td);
-			}
-			body.append(tr);
-		}
-
-		$("#pigJob" + tableName.charAt(0).toUpperCase() + tableName.substring(1)).show();
-	},
-	renderPigSummaryTable: function() {
-		this.renderPigTable("summary", this.model.get("pigSummary"));
-	},
-	renderPigStatsTable: function() {
-		this.renderPigTable("stats", this.model.get("pigStats"));
-	},
-	renderHiveTable: function() {
-		var hiveSummary = this.model.get("hiveSummary");
-		var queries = hiveSummary.hiveQueries;
-		var queryJobs = hiveSummary.hiveQueryJobs;
-
-		// Set up table column headers
-		var header = $("#hiveTableHeader");
-		var tr = document.createElement("tr");
-
-		var headers;
-		if (this.model.get("hasCumulativeCPU")) {
-			headers = ["Query","Job","Map","Reduce","Cumulative CPU","HDFS Read","HDFS Write"];
-		} else {
-			headers = ["Query","Job","Map","Reduce","HDFS Read","HDFS Write"];
-		}
-
-		var i;
-		for (i = 0; i < headers.length; i++) {
-			var th = document.createElement("th");
-			$(th).text(headers[i]);
-			$(tr).append(th);
-		}
-		header.html(tr);
-		
-		// Construct table body
-		var oldBody = $("#hiveTableBody");
-		var newBody = $(document.createElement("tbody")).attr("id", "hiveTableBody");
-		for (i = 0; i < queries.length; i++) {
-			// new query
-			tr = document.createElement("tr");
-			var td = document.createElement("td");
-			$(td).html("<b>" + queries[i] + "</b>");
-			$(tr).append(td);
-			
-			var jobs = queryJobs[i];
-			if (jobs != null) {
-				// add first job for this query
-				var jobValues = jobs[0];
-				var j;
-				for (j = 0; j < jobValues.length; j++) {
-					td = document.createElement("td");
-					$(td).html(jobValues[j]);
-					$(tr).append(td);
-				}
-				newBody.append(tr);
-				
-				// add remaining jobs for this query
-				for (j = 1; j < jobs.length; j++) {
-					jobValues = jobs[j];
-					tr = document.createElement("tr");
-					
-					// add empty cell for query column
-					td = document.createElement("td");
-					$(td).html("&nbsp;");
-					$(tr).append(td);
-					
-					// add job values
-					for (var k = 0; k < jobValues.length; k++) {
-						td = document.createElement("td");
-						$(td).html(jobValues[k]);
-						$(tr).append(td);
-					}
-					newBody.append(tr);
-				}
-				
-			} else {
-				newBody.append(tr);
-			}
-		}
-		oldBody.replaceWith(newBody);
-
-		$("#hiveJobSummary").show();
-	}
-});
-
-var jobTabView;
-azkaban.JobTabView = Backbone.View.extend({
-	events: {
-		'click #jobSummaryViewLink': 'handleJobSummaryViewLinkClick',
-		'click #jobLogViewLink': 'handleJobLogViewLinkClick'
-	},
-
-	initialize: function(settings) {
-		var selectedView = settings.selectedView;
-		if (selectedView == 'summary') {
-			this.handleJobSummaryViewLinkClick();
-		}
-		else {
-			this.handleJobLogViewLinkClick();
-		}
-	},
-
-	handleJobLogViewLinkClick: function() {
-		$('#jobSummaryViewLink').removeClass('active');
-		$('#jobSummaryView').hide();
-		$('#jobLogViewLink').addClass('active');
-		$('#jobLogView').show();
-	},
-	
-	handleJobSummaryViewLinkClick: function() {
-		$('#jobSummaryViewLink').addClass('active');
-		$('#jobSummaryView').show();
-		$('#jobLogViewLink').removeClass('active');
-		$('#jobLogView').hide();
-	},
-});
-
 var showDialog = function(title, message) {
   $('#messageTitle').text(title);
   $('#messageBox').text(message);
@@ -296,31 +55,10 @@ var showDialog = function(title, message) {
 }
 
 $(function() {
-	var logDataModel = new azkaban.LogDataModel();
-	
+  var jobLogModel = new azkaban.JobLogModel();
 	jobLogView = new azkaban.JobLogView({
 		el: $('#jobLogView'), 
-		model: logDataModel
-	});
-
-	jobSummaryView = new azkaban.JobSummaryView({
-		el: $('#jobSummaryView'), 
-		model: logDataModel
+		model: jobLogModel 
 	});
-
-	jobTabView = new azkaban.JobTabView({
-		el: $('#headertabs')
-	});
-
-	logDataModel.refresh();
-
-	if (window.location.hash) {
-		var hash = window.location.hash;
-		if (hash == '#logs') {
-			jobTabView.handleJobLogViewLinkClick();
-		}
-		else if (hash == '#summary') {
-			jobTabView.handleJobSummaryViewLinkClick();
-		}
-	}
+  jobLogModel.refresh();
 });
diff --git a/src/web/js/azkaban/view/project-permissions.js b/src/web/js/azkaban/view/project-permissions.js
index fbd332c..08bf865 100644
--- a/src/web/js/azkaban/view/project-permissions.js
+++ b/src/web/js/azkaban/view/project-permissions.js
@@ -67,9 +67,12 @@ azkaban.RemoveProxyView = Backbone.View.extend({
 	
 	display: function(proxyName) {
 		this.el.proxyName = proxyName;
-		$("#proxyRemoveMsg").text("Removing proxy user '" + proxyName + "'");
-		$(this.el).modal();
+		$("#remove-proxy-msg").text("Removing proxy user '" + proxyName + "'");
+		$(this.el).modal().on('hide.bs.modal', function(e) {
+      $('#remove-proxy-error-msg').hide();
+    });
 	},
+
 	handleRemoveProxy: function() {
 		var requestURL = contextURL + "/manager";
 		var proxyName = this.el.proxyName;
@@ -81,8 +84,8 @@ azkaban.RemoveProxyView = Backbone.View.extend({
 		var successHandler = function(data) {
 			console.log("Output");
 			if (data.error) {
-				$("#removeProxyErrorMsg").text(data.error);
-				$("#removeProxyErrorMsg").show();
+				$("#remove-proxy-error-msg").text(data.error);
+				$("#remove-proxy-error-msg").slideDown();
 				return;
 			}
 			var replaceURL = requestURL + "?project=" + projectName +"&permissions";
@@ -104,12 +107,14 @@ azkaban.AddProxyView = Backbone.View.extend({
 	},
 	
 	display: function() {
-		$(this.el).modal();
+		$(this.el).modal().on('hide.bs.modal', function(e) {
+      $('#add-proxy-error-msg').hide();
+    });
 	},
 	
 	handleAddProxy: function() {
 		var requestURL = contextURL + "/manager";
-		var name = $('#proxy-user-box').val();
+		var name = $('#proxy-user-box').val().trim();
 		var requestData = {
 			"project": projectName, 
 			"name": name, 
@@ -119,8 +124,8 @@ azkaban.AddProxyView = Backbone.View.extend({
 		var successHandler = function(data) {
 			console.log("Output");
 			if (data.error) {
-				$("#proxyErrorMsg").text(data.error);
-				$("#proxyErrorMsg").show();
+				$("#add-proxy-error-msg").text(data.error);
+				$("#add-proxy-error-msg").slideDown();
 				return;
 			}
 			
@@ -204,7 +209,9 @@ azkaban.ChangePermissionView= Backbone.View.extend({
 		this.changeCheckbox();
 		
 		changePermissionView.render();
-		$('#change-permission').modal();
+		$('#change-permission').modal().on('hide.bs.modal', function(e) {
+      $('#change-permission-error-msg').hide();
+    });
 	},
 	
 	render: function() {
@@ -262,7 +269,7 @@ azkaban.ChangePermissionView= Backbone.View.extend({
 			$("#change-btn").text("Commit");
 		}
 		else {
-			if(	this.newPerm) {
+			if (this.newPerm) {
 				$("#change-btn").disabled = true;
 				$("#change-btn").addClass("btn-disabled");
 			}
@@ -274,7 +281,7 @@ azkaban.ChangePermissionView= Backbone.View.extend({
 	
 	handleChangePermissions : function(evt) {
 		var requestURL = contextURL + "/manager";
-		var name = $('#user-box').val();
+		var name = $('#user-box').val().trim();
 		var command = this.newPerm ? "addPermission" : "changePermission";
 		var group = this.group;
 		
@@ -295,8 +302,8 @@ azkaban.ChangePermissionView= Backbone.View.extend({
 		var successHandler = function(data) {
 			console.log("Output");
 			if (data.error) {
-				$("#errorMsg").text(data.error);
-				$("#errorMsg").show();
+				$("#change-permission-error-msg").text(data.error);
+				$("#change-permission-error-msg").slideDown();
 				return;
 			}
 			
diff --git a/src/web/js/azkaban/view/time-graph.js b/src/web/js/azkaban/view/time-graph.js
index 7cb3f6f..0f53fa4 100644
--- a/src/web/js/azkaban/view/time-graph.js
+++ b/src/web/js/azkaban/view/time-graph.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
@@ -19,7 +19,7 @@ $.namespace('azkaban');
 azkaban.TimeGraphView = Backbone.View.extend({
 	events: {
 	},
-	
+
 	initialize: function(settings) {
 		this.model.bind('render', this.render, this);
 		this.model.bind('change:page', this.render, this);
@@ -36,7 +36,7 @@ azkaban.TimeGraphView = Backbone.View.extend({
 
     // Array of points to be passed to Morris.
     var data = [];
-    
+
     // Map of y value to index for faster look-up in the lineColorsCallback to
     // get the status for each point.
     var indexMap = {};
@@ -47,11 +47,19 @@ azkaban.TimeGraphView = Backbone.View.extend({
       }
       var startTime = series[i].startTime;
       var endTime = series[i].endTime;
+      if (startTime == -1 && endTime == -1) {
+        console.log("Ignoring data point with both start and end time invalid.");
+        continue;
+      }
+
+      var duration = 0;
+      if (endTime != -1 && startTime != -1) {
+        duration = endTime - startTime;
+      }
       if (endTime == -1) {
         endTime = new Date().getTime();
       }
-      var duration = endTime - startTime;
-      data.push({ 
+      data.push({
         time: endTime,
         duration: duration
       });
@@ -85,8 +93,8 @@ azkaban.TimeGraphView = Backbone.View.extend({
       else if (status == 'PAUSED') {
         return '#c92123';
       }
-      else if (status == 'FAILED' || 
-          status == 'FAILED_FINISHING' || 
+      else if (status == 'FAILED' ||
+          status == 'FAILED_FINISHING' ||
           status == 'KILLED') {
         return '#cc0000';
       }
@@ -105,7 +113,7 @@ azkaban.TimeGraphView = Backbone.View.extend({
       // is the index into Morris's internal array of data sorted in ascending
       // x order.
       var status = series[options.data.length - index - 1].status;
-      return content + 
+      return content +
           '<div class="morris-hover-point">Status: ' + status + '</div>';
     };