azkaban-aplcache

Details

diff --git a/src/java/azkaban/executor/ExecutorManager.java b/src/java/azkaban/executor/ExecutorManager.java
index e3ff56c..8370103 100644
--- a/src/java/azkaban/executor/ExecutorManager.java
+++ b/src/java/azkaban/executor/ExecutorManager.java
@@ -8,6 +8,7 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -29,6 +30,7 @@ import org.apache.http.client.utils.URIBuilder;
 import org.apache.http.impl.client.BasicResponseHandler;
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.apache.log4j.Logger;
+import org.joda.time.DateTime;
 
 import azkaban.executor.ExecutableFlow.Status;
 import azkaban.flow.Flow;
@@ -45,8 +47,10 @@ public class ExecutorManager {
 	private static final String ACTIVE_DIR = ".active";
 	private static final String ARCHIVE_DIR = ".archive";
 	private static Logger logger = Logger.getLogger(ExecutorManager.class);
-	private static final long ACCESS_ERROR_THRESHOLD = 60000;
+	// 30 seconds of retry before failure.
+	private static final long ACCESS_ERROR_THRESHOLD = 30000;
 	private static final int UPDATE_THREAD_MS = 1000;
+
 	private File basePath;
 
 	private AtomicInteger counter = new AtomicInteger();
@@ -153,6 +157,71 @@ public class ExecutorManager {
 		return execFlows;
 	}
 
+	public List<ExecutionReference> getFlowHistory(int numResults, int skip) {
+		ArrayList<ExecutionReference> searchFlows = new ArrayList<ExecutionReference>();
+
+		for (ExecutableFlow flow: runningFlows.values()) {
+			if (skip > 0) {
+				skip--;
+			}
+			else {
+				ExecutionReference ref = new ExecutionReference(flow);
+				searchFlows.add(ref);
+				if (searchFlows.size() == numResults) {
+					Collections.sort(searchFlows);
+					return searchFlows;
+				}
+			}
+		}
+		
+		File archivePath = new File(basePath, ARCHIVE_DIR);
+		if (!archivePath.exists()) {
+			return searchFlows;
+		}
+		
+		File[] archivePartitionsDir = archivePath.listFiles();
+		for (File archivePartition: archivePartitionsDir) {
+			File[] listArchivePartitions = archivePartition.listFiles();
+			if (skip > listArchivePartitions.length) {
+				skip -= listArchivePartitions.length;
+				continue;
+			}
+			
+			Arrays.sort(listArchivePartitions);
+			for (int i = listArchivePartitions.length - 1; i >= 0; --i) {
+				if (skip > 0) {
+					skip--;
+				}
+				else {
+					try {
+						ExecutionReference ref = ExecutionReference.readFromDirectory(listArchivePartitions[i]);
+						searchFlows.add(ref);
+					} catch (IOException e) {
+						// TODO Auto-generated catch block
+						e.printStackTrace();
+					}
+					
+					if (searchFlows.size() == numResults) {
+						Collections.sort(searchFlows);
+						return searchFlows;
+					}
+				}
+			}
+		}
+		
+		Collections.sort(searchFlows);
+		return searchFlows;
+	}
+	
+	private boolean isBetween(long val, long from, long to) {
+		// Means that range isn't set, so we'll just say that it's okay.
+		if (to < -1) {
+			return true;
+		}
+		
+		return val >= from && val <= to;
+	}
+	
 	private void loadActiveExecutions() throws IOException, ExecutorManagerException {
 		File activeFlows = new File(basePath, ACTIVE_DIR);
 		File[] activeFlowDirs = activeFlows.listFiles();
@@ -226,7 +295,10 @@ public class ExecutorManager {
 		File referenceDir = new File(baseActiveDir, executionId);
 		
 		if (!referenceDir.exists()) {
-			File baseArchiveDir = new File(basePath, ARCHIVE_DIR);
+			// Find the partition it would be in and search.
+			String partition = getExecutableReferencePartition(executionId);
+			
+			File baseArchiveDir = new File(basePath, ARCHIVE_DIR + File.separator + partition);
 			referenceDir = new File(baseArchiveDir, executionId);
 			if (!referenceDir.exists()) {
 				throw new ExecutorManagerException("Execution id '" + executionId + "' not found. Searching " + referenceDir);
@@ -508,6 +580,14 @@ public class ExecutorManager {
 		return response;
 	}
 	
+	private String getExecutableReferencePartition(String execID) {
+		// We're partitioning the archive by the first part of the id, which should be a timestamp.
+		// Then we're taking a substring of length - 6 to lop off the bottom 5 digits effectively partitioning
+		// by 100000 millisec. We do this to have quicker searchs by pulling partitions, not full directories.
+		int index = execID.indexOf('.');
+		return execID.substring(0, index - 5);
+	}
+	
 	private void cleanFinishedJob(ExecutableFlow exFlow) throws ExecutorManagerException {
 		
 		// Write final file
@@ -522,16 +602,44 @@ public class ExecutorManager {
 			throw new ExecutorManagerException("Active reference " + activeDirectory + " doesn't exists.");
 		}
 		
-		String archiveReferencePath = ARCHIVE_DIR + File.separator + exFlow.getExecutionId(); 
-		File archiveDirectory = new File(basePath, archiveReferencePath);
+		String exId = exFlow.getExecutionId();
+		String partitionVal = getExecutableReferencePartition(exId);
+		
+		String archiveDatePartition = ARCHIVE_DIR + File.separator + partitionVal;
+		File archivePartitionDir = new File(basePath, archiveDatePartition);
+		if (!archivePartitionDir.exists()) {
+			archivePartitionDir.mkdirs();
+		}
+
+		File archiveDirectory = new File(archivePartitionDir, exFlow.getExecutionId());
 		if (archiveDirectory.exists()) {
-			logger.error("WTF!! Archive reference already exists!");
-			throw new ExecutorManagerException("Active reference " + archiveDirectory + " already exists.");
+			logger.error("Archive reference already exists. Cleaning up.");
+			try {
+				FileUtils.deleteDirectory(activeDirectory);
+			} catch (IOException e) {
+				logger.error(e);
+			}
+			
+			return;
+		}
+		
+		// Make new archive dir
+		if (!archiveDirectory.mkdirs()) {
+			throw new ExecutorManagerException("Cannot create " + archiveDirectory);
+		}
+		
+		ExecutionReference reference = new ExecutionReference(exFlow);
+		try {
+			reference.writeToDirectory(archiveDirectory);
+		} catch (IOException e) {
+			throw new ExecutorManagerException("Couldn't write execution to directory.", e);
 		}
 		
 		// Move file.
-		if (!activeDirectory.renameTo(archiveDirectory)) {
-			throw new ExecutorManagerException("Cannot move " + activeDirectory + " to " + archiveDirectory);
+		try {
+			FileUtils.deleteDirectory(activeDirectory);
+		} catch (IOException e) {
+			throw new ExecutorManagerException("Cannot cleanup active directory " + activeDirectory);
 		}
 		
 		runningFlows.remove(exFlow.getExecutionId());
@@ -613,6 +721,7 @@ public class ExecutorManager {
 							logger.error("Flow " + exFlow.getExecutionId() + " has succeeded, but the Executor says its still running with msg: " + status);
 							if (System.currentTimeMillis() - exFlow.getLastCheckedTime() > ACCESS_ERROR_THRESHOLD) {
 								exFlow.setStatus(Status.FAILED);
+								exFlow.setEndTime(System.currentTimeMillis());
 								logger.error("It's been " + ACCESS_ERROR_THRESHOLD + " ms since last update. Auto-failing the job.");
 							}
 						}
@@ -623,6 +732,7 @@ public class ExecutorManager {
 							logger.error("Flow " + exFlow.getExecutionId() + " is running, but the Executor can't find it.");
 							if (System.currentTimeMillis() - exFlow.getLastCheckedTime() > ACCESS_ERROR_THRESHOLD) {
 								exFlow.setStatus(Status.FAILED);
+								exFlow.setEndTime(System.currentTimeMillis());
 								logger.error("It's been " + ACCESS_ERROR_THRESHOLD + " ms since last update. Auto-failing the job.");
 							}
 						}
@@ -644,13 +754,16 @@ public class ExecutorManager {
 		}
 	}
 	
-	private static class ExecutionReference {
+	public static class ExecutionReference implements Comparable<ExecutionReference> {
 		private String execId;
 		private String projectId;
 		private String flowId;
 		private String userId;
 		private String execPath;
-		
+		private long startTime;
+		private long endTime;
+		private Status status;
+
 		public ExecutionReference() {
 		}
 		
@@ -660,6 +773,10 @@ public class ExecutorManager {
 			this.flowId = flow.getFlowId();
 			this.userId = flow.getSubmitUser();
 			this.execPath = flow.getExecutionPath();
+			
+			this.startTime = flow.getStartTime();
+			this.endTime = flow.getEndTime();
+			this.status = flow.getStatus();
 		}
 		
 		private Object toObject() {
@@ -669,6 +786,9 @@ public class ExecutorManager {
 			obj.put("flowId", flowId);
 			obj.put("userId", userId);
 			obj.put("execPath", execPath);
+			obj.put("startTime", startTime);
+			obj.put("endTime", endTime);
+			obj.put("status", status);
 			return obj;
 		}
 		
@@ -692,7 +812,9 @@ public class ExecutorManager {
 			reference.flowId = (String)obj.get("flowId");
 			reference.userId = (String)obj.get("userId");
 			reference.execPath = (String)obj.get("execPath");
-
+			reference.startTime = (Long)obj.get("startTime");
+			reference.endTime = (Long)obj.get("endTime");
+			reference.status = Status.valueOf((String)obj.get("status"));
 			return reference;
 		}
 		
@@ -715,5 +837,34 @@ public class ExecutorManager {
 		public String getExecPath() {
 			return execPath;
 		}
+
+		public Long getStartTime() {
+			return startTime;
+		}
+
+		public void setStartTime(Long startTime) {
+			this.startTime = startTime;
+		}
+
+		public Long getEndTime() {
+			return endTime;
+		}
+
+		public void setEndTime(Long endTime) {
+			this.endTime = endTime;
+		}
+
+		@Override
+		public int compareTo(ExecutionReference arg0) {
+			return arg0.getExecId().compareTo(execId);
+		}
+		
+		public Status getStatus() {
+			return status;
+		}
+
+		public void setStatus(Status status) {
+			this.status = status;
+		}
 	}
 }
diff --git a/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java b/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
index a2736eb..515e14b 100644
--- a/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
+++ b/src/java/azkaban/webapp/servlet/AbstractAzkabanServlet.java
@@ -35,7 +35,7 @@ import org.joda.time.DateTime;
 import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
 
-import azkaban.utils.GUIUtils;
+import azkaban.utils.WebUtils;
 import azkaban.utils.JSONUtils;
 import azkaban.utils.Props;
 import azkaban.webapp.AzkabanWebServer;
@@ -56,7 +56,7 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
 	public static final String XML_MIME_TYPE = "application/xhtml+xml";
 	public static final String JSON_MIME_TYPE = "application/json";
 
-	private static final GUIUtils utils = new GUIUtils();
+	private static final WebUtils utils = new WebUtils();
 	
 	private AzkabanWebServer application;
 	private String name;
@@ -130,6 +130,18 @@ public abstract class AbstractAzkabanServlet extends HttpServlet {
 		String p = getParam(request, name);
 		return Integer.parseInt(p);
 	}
+	
+	public int getIntParam(HttpServletRequest request, String name, int defaultVal) {
+		if (hasParam(request, name)) {
+			try {
+				return getIntParam(request, name);
+			} catch (Exception e) {
+				return defaultVal;
+			}
+		}
+		
+		return defaultVal;
+	}
 
 	public Map<String, String> getParamGroup(HttpServletRequest request, String groupName)  throws ServletException {
 		Enumeration<Object> enumerate = (Enumeration<Object>)request.getParameterNames();
diff --git a/src/java/azkaban/webapp/servlet/HistoryServlet.java b/src/java/azkaban/webapp/servlet/HistoryServlet.java
index e2b9abf..d665e5b 100644
--- a/src/java/azkaban/webapp/servlet/HistoryServlet.java
+++ b/src/java/azkaban/webapp/servlet/HistoryServlet.java
@@ -1,23 +1,68 @@
 package azkaban.webapp.servlet;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
+import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import azkaban.executor.ExecutorManager;
+import azkaban.executor.ExecutorManager.ExecutionReference;
+import azkaban.project.ProjectManager;
 import azkaban.webapp.session.Session;
 
 public class HistoryServlet extends LoginAbstractAzkabanServlet {
 
 	private static final long serialVersionUID = 1L;
+	private ProjectManager projectManager;
+	private ExecutorManager executorManager;
+	
+	@Override
+	public void init(ServletConfig config) throws ServletException {
+		super.init(config);
+		projectManager = this.getApplication().getProjectManager();
+		executorManager = this.getApplication().getExecutorManager();
+	}
 
 	@Override
 	protected void handleGet(HttpServletRequest req, HttpServletResponse resp,
 			Session session) throws ServletException, IOException {
 		Page page = newPage(req, resp, session, "azkaban/webapp/servlet/velocity/historypage.vm");
+		int pageNum = getIntParam(req, "page", 1);
+		int pageSize = getIntParam(req, "size", 16);
+		
+		if (pageNum < 0) {
+			pageNum = 1;
+		}
 		
+		List<ExecutionReference> history = executorManager.getFlowHistory(pageSize, (pageNum - 1)*pageSize);
+		page.add("flowHistory", history);
+		page.add("size", pageSize);
+		page.add("page", pageNum);
 		
+		if (pageNum == 1) {
+			page.add("previous", new PageSelection(1, pageSize, true, false));
+		}
+		page.add("next", new PageSelection(pageNum + 1, pageSize, false, false));
+		// Now for the 5 other values.
+		int pageStartValue = 1;
+		if (pageNum > 3) {
+			pageStartValue = pageNum - 2;
+		}
+		
+		page.add("page1", new PageSelection(pageStartValue, pageSize, false, pageStartValue == pageNum));
+		pageStartValue++;
+		page.add("page2", new PageSelection(pageStartValue, pageSize, false, pageStartValue == pageNum));
+		pageStartValue++;
+		page.add("page3", new PageSelection(pageStartValue, pageSize, false, pageStartValue == pageNum));
+		pageStartValue++;
+		page.add("page4", new PageSelection(pageStartValue, pageSize, false, pageStartValue == pageNum));
+		pageStartValue++;
+		page.add("page5", new PageSelection(pageStartValue, pageSize, false, pageStartValue == pageNum));
+		pageStartValue++;
 		
 		page.render();
 	}
@@ -29,4 +74,37 @@ public class HistoryServlet extends LoginAbstractAzkabanServlet {
 		
 	}
 
+	public class PageSelection {
+		private int page;
+		private int size;
+		private boolean disabled;
+		private boolean selected;
+		
+		public PageSelection(int page, int size, boolean disabled, boolean selected) {
+			this.page = page;
+			this.size = size;
+			this.disabled = disabled;
+			this.setSelected(selected);
+		}
+		
+		public int getPage() {
+			return page;
+		}
+		
+		public int getSize() {
+			return size;
+		}
+		
+		public boolean getDisabled() {
+			return disabled;
+		}
+
+		public boolean isSelected() {
+			return selected;
+		}
+
+		public void setSelected(boolean selected) {
+			this.selected = selected;
+		}
+	}
 }
diff --git a/src/java/azkaban/webapp/servlet/velocity/executionspage.vm b/src/java/azkaban/webapp/servlet/velocity/executionspage.vm
index 4a15e8b..3dd9c6f 100644
--- a/src/java/azkaban/webapp/servlet/velocity/executionspage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/executionspage.vm
@@ -34,8 +34,10 @@
 				<table id="executingJobs">
 					<thead>
 						<tr>
+							<th class="execid">Execution Id</th>
 							<th>Flow</th>
-							<th>User</th>
+							<th>Project</th>
+							<th class="user">User</th>
 							<th class="date">Start Time</th>
 							<th class="date">End Time</th>
 							<th class="elapse">Elapsed</th>
@@ -48,13 +50,17 @@
 #foreach($flow in $runningFlows)
 						<tr class="row" >
 							<td class="tb-name">
-								<a href="${context}/execution?execid=${flow.executionId}">${flow.flowId}</a>
+								<a href="${context}/executions?execid=${flow.executionId}">$utils.extractNumericalId(${flow.executionId})</a>
+							</td>
+							<td><a href="${context}/manager?project=${flow.projectId}&flow=${flow.flowId}">${flow.flowId}</a></td>
+							<td>
+								<a href="${context}/manager?project=${flow.projectId}">${flow.projectId}</a>
 							</td>
 							<td>${flow.submitUser}</td>
 							<td>$utils.formatDate(${flow.startTime})</td>
 							<td>$utils.formatDate(${flow.endTime})</td>
 							<td>$utils.formatDuration(${flow.startTime}, ${flow.endTime})</td>
-							<td>${flow.status}</td>
+							<td><div class="status ${flow.status}">$utils.formatStatus(${flow.status})</div></td>
 							<td></td>
 						</tr>
 #end
@@ -69,8 +75,10 @@
 				<table id="recentlyFinished">
 					<thead>
 						<tr>
+							<th class="execid">Execution Id</th>
 							<th>Flow</th>
-							<th>User</th>
+							<th>Project</th>
+							<th class="user">User</th>
 							<th class="date">Start Time</th>
 							<th class="date">End Time</th>
 							<th class="elapse">Elapsed</th>
@@ -83,13 +91,17 @@
 #foreach($flow in $recentlyFinished)
 						<tr class="row" >
 							<td class="tb-name">
-								<a href="${context}/executions?execid=${flow.executionId}">${flow.flowId}</a>
+								<a href="${context}/executions?execid=${flow.executionId}">$utils.extractNumericalId(${flow.executionId})</a>
+							</td>
+							<td><a href="${context}/manager?project=${flow.projectId}&flow=${flow.flowId}">${flow.flowId}</a></td>
+							<td>
+								<a href="${context}/manager?project=${flow.projectId}">${flow.projectId}</a>
 							</td>
 							<td>${flow.submitUser}</td>
 							<td>$utils.formatDate(${flow.startTime})</td>
 							<td>$utils.formatDate(${flow.endTime})</td>
 							<td>$utils.formatDuration(${flow.startTime}, ${flow.endTime})</td>
-							<td>${flow.status}</td>
+							<td><div class="status ${flow.status}">$utils.formatStatus(${flow.status})</div></td>
 							<td></td>
 						</tr>
 #end
diff --git a/src/java/azkaban/webapp/servlet/velocity/historypage.vm b/src/java/azkaban/webapp/servlet/velocity/historypage.vm
index 4296ebc..7204cee 100644
--- a/src/java/azkaban/webapp/servlet/velocity/historypage.vm
+++ b/src/java/azkaban/webapp/servlet/velocity/historypage.vm
@@ -38,7 +38,9 @@
 				<table id="executingJobs">
 					<thead>
 						<tr>
+							<th class="execid">Execution Id</th>
 							<th>Flow</th>
+							<th>Project</th>
 							<th>User</th>
 							<th class="date">Start Time</th>
 							<th class="date">End Time</th>
@@ -48,25 +50,46 @@
 						</tr>
 					</thead>
 					<tbody>
-						#if($runningFlows)
-#foreach($flow in $runningFlows)
+						#if($flowHistory)
+#foreach($flow in $flowHistory)
 						<tr class="row" >
 							<td class="tb-name">
-								<a href="${context}/execution?execid=${flow.executionId}">${flow.flowId}</a>
+								<a href="${context}/executions?execid=${flow.execId}">$utils.extractNumericalId(${flow.execId})</a>
 							</td>
-							<td>${flow.submitUser}</td>
+							<td class="tb-name">
+								<a href="${context}/manager?project=${flow.projectId}&flow=${flow.flowId}">${flow.flowId}</a>
+							</td>
+							<td>
+								<a href="${context}/manager?project=${flow.projectId}">${flow.projectId}</a>
+							</td>
+							<td>${flow.userId}</td>
 							<td>$utils.formatDate(${flow.startTime})</td>
 							<td>$utils.formatDate(${flow.endTime})</td>
 							<td>$utils.formatDuration(${flow.startTime}, ${flow.endTime})</td>
-							<td>${flow.status}</td>
+							<td><div class="status ${flow.status}">$utils.formatStatus(${flow.status})</div></td>
 							<td></td>
 						</tr>
 #end
 #else
-						<tr><td class="last">No History Found</td></tr>
+						<tr><td class="last">No History Results Found</td></tr>
 #end
 					</tbody>
 				</table>
+				
+				<div id="pageSelection" class="nonjavascript">
+					<ul>
+		
+						<li id="previous" class="first"><a href="${context}/history?page=${previous.page}&size=${previous.size}"><span class="arrow">&larr;</span>Previous</a></li>
+				
+						<li id="page1" #if($page1.selected) class="selected" #end><a href="${context}/history?page=${page1.page}&size=${page1.size}">${page1.page}</a></li>
+						<li id="page2" #if($page2.selected) class="selected" #end><a href="${context}/history?page=${page2.page}&size=${page2.size}">${page2.page}</a></li>
+						<li id="page3" #if($page3.selected) class="selected" #end><a href="${context}/history?page=${page3.page}&size=${page3.size}">${page3.page}</a></li>
+						<li id="page4" #if($page4.selected) class="selected" #end><a href="${context}/history?page=${page4.page}&size=${page4.size}">${page4.page}</a></li>
+						<li id="page5" #if($page5.selected) class="selected" #end><a href="${context}/history?page=${page5.page}&size=${page5.size}">${page5.page}</a></li>
+
+						<li id="next"><a href="${context}/history?page=${next.page}&size=${next.size}">Next<span class="arrow">&rarr;</span></a></li>
+					</ul>
+				</div>
 			</div>
 		</div>
 	</body>
diff --git a/src/web/css/azkaban.css b/src/web/css/azkaban.css
index 8d7096e..13163f8 100644
--- a/src/web/css/azkaban.css
+++ b/src/web/css/azkaban.css
@@ -1366,6 +1366,10 @@ span.sublabel {
 	margin-top: 5px;
 }
 
+#pageSelection ul li:hover {
+	background-color: #D5F2FF;
+}
+
 #pageSelection ul li.first {
 	border-left: 1px solid #CCC; 
 }
@@ -1470,6 +1474,18 @@ span.sublabel {
 	width: 140px;
 }
 
+.executionInfo table th.execid {
+	width: 150px;
+}
+
+.executionInfo table th.project {
+	width: 200px;
+}
+
+.executionInfo table th.user {
+	width: 60px;
+}
+
 .executionInfo table th.elapse {
 	width: 90px;
 }
@@ -1502,6 +1518,49 @@ span.sublabel {
 	height: 20px;
 }
 
+td .status {
+	-moz-border-radius: 2px;
+	border-radius: 2px;
+
+	padding: 2px 2px;
+	color: #FFF;
+	text-align: center;
+	margin-top: 2px;
+}
+
+td .status.SUCCEEDED {
+	background-color: #82B859;
+}
+
+td .status.FAILED {
+	background-color: #C82123;
+}
+
+td .status.READY {
+	background-color: #C82123;
+}
+
+td .status.RUNNING {
+	background-color: #3398CC;	
+}
+
+td .status.FAILED_FINISHING {
+	background-color: #F19153;	
+}
+
+td .status.DISABLED {
+	background-color: #AAA;	
+}
+
+td .status.KILLED {
+	background-color: #000;
+}
+
+td .status.UNKNOWN {
+	background-color: #CCC;
+}
+
+
 .flow-header {
 	height: 48px;
 }
@@ -1510,6 +1569,11 @@ span.sublabel {
 	width: 280px;
 	margin: 4px;
 	background-color: #e2e4e3;
+	border: 1px solid #FFF;
+}
+
+tr:hover .outerProgress {
+	background-color: #F8F8F8;
 }
 
 .progressBox {
diff --git a/src/web/js/azkaban.exflow.view.js b/src/web/js/azkaban.exflow.view.js
index c95520d..c639026 100644
--- a/src/web/js/azkaban.exflow.view.js
+++ b/src/web/js/azkaban.exflow.view.js
@@ -1,6 +1,17 @@
 $.namespace('azkaban');
 
 var statusList = ["FAILED", "FAILED_FINISHING", "SUCCEEDED", "RUNNING", "WAITING", "KILLED", "DISABLED", "READY", "UNKNOWN"];
+var statusStringMap = {
+	"FAILED": "Failed",
+	"SUCCEEDED": "Success",
+	"FAILED_FINISHING": "Running w/Failure",
+	"RUNNING": "Running",
+	"WAITING": "Waiting",
+	"KILLED": "Killed",
+	"DISABLED": "Disabled",
+	"READY": "Ready",
+	"UNKNOWN": "Unknown"
+};
 
 var handleJobMenuClick = function(action, el, pos) {
 	var jobid = el[0].jobid;
@@ -578,7 +589,9 @@ azkaban.ExecutionListView = Backbone.View.extend({
 					this.addNodeRow(node);
 				}
 				
-				$("#" + node.id + "-status").text(node.status);
+				var div = $("#" + node.id + "-status-div");
+				div.text(statusStringMap[node.status]);
+				$(div).attr("class", "status " + node.status);
 				
 				var startdate = new Date(node.startTime);
 				$("#" + node.id + "-start").text(getDateFormat(startdate));
@@ -680,6 +693,11 @@ azkaban.ExecutionListView = Backbone.View.extend({
 		$(a).text(node.id);
 		$(tdName).append(a);
 
+		var status = document.createElement("div");
+		$(status).addClass("status");
+		$(status).attr("id", node.id + "-status-div");
+		tdStatus.appendChild(status);
+
 		executingBody.append(tr);
 	}
 });
diff --git a/src/web/js/azkaban.flow.view.js b/src/web/js/azkaban.flow.view.js
index c952eda..868d8d3 100644
--- a/src/web/js/azkaban.flow.view.js
+++ b/src/web/js/azkaban.flow.view.js
@@ -1,5 +1,17 @@
 $.namespace('azkaban');
 
+var statusStringMap = {
+	"FAILED": "Failed",
+	"SUCCEEDED": "Success",
+	"FAILED_FINISHING": "Running w/Failure",
+	"RUNNING": "Running",
+	"WAITING": "Waiting",
+	"KILLED": "Killed",
+	"DISABLED": "Disabled",
+	"READY": "Ready",
+	"UNKNOWN": "Unknown"
+};
+
 var handleJobMenuClick = function(action, el, pos) {
 	var jobid = el[0].jobid;
 	var requestURL = contextURL + "/manager?project=" + projectName + "&flow=" + flowName + "&job=" + jobid;
@@ -614,7 +626,7 @@ azkaban.ExecutionsView = Backbone.View.extend({
 	initialize: function(settings) {
 		this.model.bind('change:view', this.handleChangeView, this);
 		this.model.bind('render', this.render, this);
-		this.model.set({page: 1, pageSize: 20});
+		this.model.set({page: 1, pageSize: 16});
 		this.model.bind('change:page', this.handlePageChange, this);
 	},
 	render: function(evt) {
@@ -667,7 +679,11 @@ azkaban.ExecutionsView = Backbone.View.extend({
 			row.appendChild(tdElapsed);
 			
 			var tdStatus = document.createElement("td");
-			$(tdStatus).text(executions[i].status);
+			var status = document.createElement("div");
+			$(status).addClass("status");
+			$(status).addClass(executions[i].status);
+			$(status).text(statusStringMap[executions[i].status]);
+			tdStatus.appendChild(status);
 			row.appendChild(tdStatus);
 
 			var tdAction = document.createElement("td");