Add coloring to Test Planner machine view table cells.

Signed-off-by: James Ren <jamesren@google.com>



git-svn-id: http://test.kernel.org/svn/autotest/trunk@4485 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/client/src/autotest/common/AbstractStatusSummary.java b/frontend/client/src/autotest/common/AbstractStatusSummary.java
new file mode 100644
index 0000000..c130003
--- /dev/null
+++ b/frontend/client/src/autotest/common/AbstractStatusSummary.java
@@ -0,0 +1,58 @@
+package autotest.common;
+
+
+public abstract class AbstractStatusSummary {
+    public static final String BLANK_COLOR = "status_blank";
+    private static final ColorMapping[] CELL_COLOR_MAP = {
+        // must be in descending order of percentage
+        new ColorMapping(95, "status_95"),
+        new ColorMapping(90, "status_90"),
+        new ColorMapping(85, "status_85"),
+        new ColorMapping(75, "status_75"),
+        new ColorMapping(1, "status_bad"),
+        new ColorMapping(0, "status_none"),
+    };
+
+    /**
+     * Stores a CSS class for pass rates and the minimum passing percentage required
+     * to have that class.
+     */
+    private static class ColorMapping {
+        // store percentage as int so we can reprint it consistently
+        public int minPercent;
+        public String cssClass;
+
+        public ColorMapping(int minPercent, String cssClass) {
+            this.minPercent = minPercent;
+            this.cssClass = cssClass;
+        }
+
+        public boolean matches(double ratio) {
+            return ratio * 100 >= minPercent;
+        }
+    }
+
+    public String formatStatusCounts() {
+        String text = getPassed() + " / " + getComplete();
+        if (getIncomplete() > 0) {
+            text += " (" + getIncomplete() + " incomplete)";
+        }
+        return text;
+    }
+
+    public String getCssClass() {
+        if (getComplete() == 0) {
+            return BLANK_COLOR;
+        }
+        double ratio = (double) getPassed() / getComplete();
+        for (ColorMapping mapping : CELL_COLOR_MAP) {
+            if (mapping.matches(ratio))
+                return mapping.cssClass;
+        }
+        throw new RuntimeException("No color map match for ratio " + ratio);
+    }
+
+    protected abstract int getPassed();
+    protected abstract int getComplete();
+    protected abstract int getIncomplete();
+}
diff --git a/frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java b/frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java
index 822ac22..381c0b6 100644
--- a/frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java
+++ b/frontend/client/src/autotest/common/spreadsheet/Spreadsheet.java
@@ -33,7 +33,7 @@
 
 public class Spreadsheet extends Composite
       implements ScrollHandler, ClickHandler, ContextMenuHandler {
-    
+
     private static final int MIN_TABLE_SIZE_PX = 90;
     private static final int WINDOW_BORDER_PX = 15;
     private static final int SCROLLBAR_FUDGE = 16;
@@ -42,7 +42,7 @@
     private static final int TD_BORDER_PX = 1;
     private static final String HIGHLIGHTED_CLASS = "highlighted";
     private static final int CELLS_PER_ITERATION = 1000;
-    
+
     private Header rowFields, columnFields;
     private List<Header> rowHeaderValues = new ArrayList<Header>();
     private List<Header> columnHeaderValues = new ArrayList<Header>();
@@ -57,13 +57,13 @@
     private Panel rowHeadersClipPanel, columnHeadersClipPanel;
     private ScrollPanel scrollPanel = new ScrollPanel(dataTable);
     private TableRenderer renderer = new TableRenderer();
-    
+
     private SpreadsheetListener listener;
-    
+
     public interface SpreadsheetListener {
         public void onCellClicked(CellInfo cellInfo, boolean isRightClick);
     }
-    
+
     public static interface Header extends List<String> {}
     public static class HeaderImpl extends ArrayList<String> implements Header {
         public HeaderImpl() {
@@ -77,49 +77,49 @@
             return new HeaderImpl(baseType);
         }
     }
-    
+
     public static class CellInfo {
         public Header row, column;
         public String contents;
-        public String color;
+        public String cssClass;
         public Integer widthPx, heightPx;
         public int rowSpan = 1, colSpan = 1;
         public int testCount = 0;
         public int testIndex;
-        
+
         public CellInfo(Header row, Header column, String contents) {
             this.row = row;
             this.column = column;
             this.contents = contents;
         }
-        
+
         public boolean isHeader() {
             return !isEmpty() && (row == null || column == null);
         }
-        
+
         public boolean isEmpty() {
             return row == null && column == null;
         }
     }
-    
+
     private class RenderCommand implements IncrementalCommand {
         private int state = 0;
         private int rowIndex = 0;
         private IncrementalCommand onFinished;
-        
+
         public RenderCommand(IncrementalCommand onFinished) {
             this.onFinished = onFinished;
         }
-        
+
         private void renderSomeRows() {
-            renderer.renderRowsAndAppend(dataTable, dataCells, 
+            renderer.renderRowsAndAppend(dataTable, dataCells,
                                          rowIndex, rowsPerIteration, true);
             rowIndex += rowsPerIteration;
             if (rowIndex > dataCells.length) {
                 state++;
             }
         }
-        
+
         public boolean execute() {
             switch (state) {
                 case 0:
@@ -163,7 +163,7 @@
                     DeferredCommand.addCommand(onFinished);
                     return false;
             }
-            
+
             state++;
             return true;
         }
@@ -172,29 +172,29 @@
     public Spreadsheet() {
         dataTable.setStyleName("spreadsheet-data");
         killPaddingAndSpacing(dataTable);
-        
+
         rowHeaders.setStyleName("spreadsheet-headers");
         killPaddingAndSpacing(rowHeaders);
         rowHeadersClipPanel = wrapWithClipper(rowHeaders);
-        
+
         columnHeaders.setStyleName("spreadsheet-headers");
         killPaddingAndSpacing(columnHeaders);
         columnHeadersClipPanel = wrapWithClipper(columnHeaders);
-        
+
         scrollPanel.setStyleName("spreadsheet-scroller");
         scrollPanel.setAlwaysShowScrollBars(true);
         scrollPanel.addScrollHandler(this);
-        
+
         parentTable.setStyleName("spreadsheet-parent");
         killPaddingAndSpacing(parentTable);
         parentTable.setWidget(0, 1, columnHeadersClipPanel);
         parentTable.setWidget(1, 0, rowHeadersClipPanel);
         parentTable.setWidget(1, 1, scrollPanel);
-        
+
         setupTableInput(dataTable);
         setupTableInput(rowHeaders);
         setupTableInput(columnHeaders);
-        
+
         initWidget(parentTable);
     }
 
@@ -207,7 +207,7 @@
         table.setCellSpacing(0);
         table.setCellPadding(0);
     }
-    
+
     /*
      * Wrap a widget with a panel that will clip its contents rather than grow
      * too much.
@@ -218,12 +218,12 @@
         wrapper.setStyleName("clipper");
         return wrapper;
     }
-    
+
     public void setHeaderFields(Header rowFields, Header columnFields) {
         this.rowFields = rowFields;
         this.columnFields = columnFields;
     }
-    
+
     private void addHeader(List<Header> headerList, Map<Header, Integer> headerMap,
                           List<String> header) {
         Header headerObject = HeaderImpl.fromBaseType(header);
@@ -231,28 +231,28 @@
         headerList.add(headerObject);
         headerMap.put(headerObject, headerMap.size());
     }
-    
+
     public void addRowHeader(List<String> header) {
         addHeader(rowHeaderValues, rowHeaderMap, header);
     }
-    
+
     public void addColumnHeader(List<String> header) {
         addHeader(columnHeaderValues, columnHeaderMap, header);
     }
-    
+
     private int getHeaderPosition(Map<Header, Integer> headerMap, Header header) {
         assert headerMap.containsKey(header);
         return headerMap.get(header);
     }
-    
+
     private int getRowPosition(Header rowHeader) {
         return getHeaderPosition(rowHeaderMap, rowHeader);
     }
-    
+
     private int getColumnPosition(Header columnHeader) {
         return getHeaderPosition(columnHeaderMap, columnHeader);
     }
-    
+
     /**
      * Must be called after adding headers but before adding data
      */
@@ -268,14 +268,14 @@
         }
         return dataCells[row][column];
     }
-    
+
     private CellInfo getCellInfo(CellInfo[][] cells, int row, int column) {
         if (cells[row][column] == null) {
             cells[row][column] = new CellInfo(null, null, " ");
         }
         return cells[row][column];
     }
-    
+
     /**
      * Render the data into HTML tables.  Done through a deferred command.
      */
@@ -287,25 +287,25 @@
         renderer.renderRows(rowHeaders, rowHeaderCells, false);
         renderer.renderRows(columnHeaders, columnHeaderCells, false);
     }
-    
+
     public void computeRowsPerIteration() {
         int cellsPerRow = columnHeaderValues.size();
         rowsPerIteration = Math.max(CELLS_PER_ITERATION / cellsPerRow, 1);
         dataTable.setRowsPerFragment(rowsPerIteration);
     }
-    
+
     private void computeHeaderCells() {
         rowHeaderCells = new CellInfo[rowHeaderValues.size()][rowFields.size()];
         fillHeaderCells(rowHeaderCells, rowFields, rowHeaderValues, true);
-        
+
         columnHeaderCells = new CellInfo[columnFields.size()][columnHeaderValues.size()];
         fillHeaderCells(columnHeaderCells, columnFields, columnHeaderValues, false);
     }
-    
+
     /**
      * TODO (post-1.0) - this method needs good cleanup and documentation
      */
-    private void fillHeaderCells(CellInfo[][] cells, Header fields, List<Header> headerValues, 
+    private void fillHeaderCells(CellInfo[][] cells, Header fields, List<Header> headerValues,
                                  boolean isRows) {
         int headerSize = fields.size();
         String[] lastFieldValue = new String[headerSize];
@@ -338,7 +338,7 @@
             }
         }
     }
-    
+
     private String formatHeader(String field, String value) {
         if (value.equals("")) {
             return BLANK_STRING;
@@ -375,7 +375,7 @@
             getCellInfo(to, row, lastColumn).heightPx = height - 2 * CELL_PADDING_PX;
         }
     }
-    
+
     private void matchColumnWidths(HTMLTable from, CellInfo[][] to) {
         int lastToRow = to.length - 1;
         int lastFromRow = from.getRowCount() - 1;
@@ -384,7 +384,7 @@
             getCellInfo(to, lastToRow, column).widthPx = width - 2 * CELL_PADDING_PX;
         }
     }
-    
+
     protected String getTableCellText(HTMLTable table, int row, int column) {
         Element td = table.getCellFormatter().getElement(row, column);
         Element div = td.getFirstChildElement();
@@ -403,27 +403,27 @@
         columnHeaderMap.clear();
         dataCells = rowHeaderCells = columnHeaderCells = null;
         dataTable.reset();
-        
+
         setRowHeadersOffset(0);
         setColumnHeadersOffset(0);
     }
-    
+
     /**
      * Make the spreadsheet fill the available window space to the right and bottom
      * of its position.
      */
     public void fillWindow(boolean useTableSize) {
-        int newHeightPx = Window.getClientHeight() - (columnHeaders.getAbsoluteTop() + 
+        int newHeightPx = Window.getClientHeight() - (columnHeaders.getAbsoluteTop() +
                                                       columnHeaders.getOffsetHeight());
         newHeightPx = adjustMaxDimension(newHeightPx);
-        int newWidthPx = Window.getClientWidth() - (rowHeaders.getAbsoluteLeft() + 
+        int newWidthPx = Window.getClientWidth() - (rowHeaders.getAbsoluteLeft() +
                                                     rowHeaders.getOffsetWidth());
         newWidthPx = adjustMaxDimension(newWidthPx);
         if (useTableSize) {
             newHeightPx = Math.min(newHeightPx, rowHeaders.getOffsetHeight());
             newWidthPx = Math.min(newWidthPx, columnHeaders.getOffsetWidth());
         }
-        
+
         // apply the changes all together
         rowHeadersClipPanel.setHeight(getSizePxString(newHeightPx));
         columnHeadersClipPanel.setWidth(getSizePxString(newWidthPx));
@@ -432,18 +432,18 @@
     }
 
     /**
-     * Adjust a maximum table dimension to allow room for edge decoration and 
+     * Adjust a maximum table dimension to allow room for edge decoration and
      * always maintain a minimum height
      */
     protected int adjustMaxDimension(int maxDimensionPx) {
-        return Math.max(maxDimensionPx - WINDOW_BORDER_PX - SCROLLBAR_FUDGE, 
+        return Math.max(maxDimensionPx - WINDOW_BORDER_PX - SCROLLBAR_FUDGE,
                         MIN_TABLE_SIZE_PX);
     }
 
     protected String getSizePxString(int sizePx) {
         return sizePx + "px";
     }
-    
+
     /**
      * Ensure the row header clip panel allows the full width of the row headers
      * to display.
@@ -452,11 +452,11 @@
         int width = rowHeaders.getOffsetWidth();
         rowHeadersClipPanel.setWidth(getSizePxString(width));
     }
-    
+
     private Element getCellElement(HTMLTable table, int row, int column) {
         return table.getCellFormatter().getElement(row, column);
     }
-    
+
     private Element getCellElement(CellInfo cellInfo) {
         assert cellInfo.row != null || cellInfo.column != null;
         Element tdElement;
@@ -465,25 +465,25 @@
         } else if (cellInfo.column == null) {
             tdElement = getCellElement(rowHeaders, getRowPosition(cellInfo.row), 0);
         } else {
-            tdElement = getCellElement(dataTable, getRowPosition(cellInfo.row), 
+            tdElement = getCellElement(dataTable, getRowPosition(cellInfo.row),
                                                   getColumnPosition(cellInfo.column));
         }
         Element cellElement = tdElement.getFirstChildElement();
         assert cellElement != null;
         return cellElement;
     }
-    
+
     protected int getColumnWidth(HTMLTable table, int column) {
         // using the column formatter doesn't seem to work
         int numRows = table.getRowCount();
-        return table.getCellFormatter().getElement(numRows - 1, column).getOffsetWidth() - 
+        return table.getCellFormatter().getElement(numRows - 1, column).getOffsetWidth() -
                TD_BORDER_PX;
     }
-    
+
     protected int getRowHeight(HTMLTable table, int row) {
         // see getColumnWidth()
         int numCols = table.getCellCount(row);
-        return table.getCellFormatter().getElement(row, numCols - 1).getOffsetHeight() - 
+        return table.getCellFormatter().getElement(row, numCols - 1).getOffsetHeight() -
                TD_BORDER_PX;
     }
 
@@ -494,7 +494,7 @@
     public void onScroll(ScrollEvent event) {
         int scrollLeft = scrollPanel.getHorizontalScrollPosition();
         int scrollTop = scrollPanel.getScrollPosition();
-        
+
         setColumnHeadersOffset(-scrollLeft);
         setRowHeadersOffset(-scrollTop);
     }
@@ -506,26 +506,26 @@
     protected void setColumnHeadersOffset(int offset) {
         columnHeaders.getElement().getStyle().setPropertyPx("left", offset);
     }
-    
+
     @Override
     public void onClick(ClickEvent event) {
         handleEvent(event, false);
     }
-    
+
     @Override
     public void onContextMenu(ContextMenuEvent event) {
         handleEvent(event, true);
     }
-    
+
     private void handleEvent(DomEvent<?> event, boolean isRightClick) {
         if (listener == null)
             return;
-        
+
         assert event.getSource() instanceof RightClickTable;
         HTMLTable.Cell tableCell = ((RightClickTable) event.getSource()).getCellForDomEvent(event);
         int row = tableCell.getRowIndex();
         int column = tableCell.getCellIndex();
-        
+
         CellInfo[][] cells;
         if (event.getSource() == rowHeaders) {
             cells = rowHeaderCells;
@@ -541,12 +541,12 @@
         CellInfo cell = cells[row][column];
         if (cell == null || cell.isEmpty())
             return; // don't report clicks on empty cells
-        
+
         listener.onCellClicked(cell, isRightClick);
     }
 
     /**
-     * In HTMLTables, a cell with rowspan > 1 won't count in column indices for the extra rows it 
+     * In HTMLTables, a cell with rowspan > 1 won't count in column indices for the extra rows it
      * spans, which will mess up column indices for other cells in those rows.  This method adjusts
      * the column index passed to onCellClicked() to account for that.
      */
@@ -556,7 +556,7 @@
                 return i + column;
             }
         }
-        
+
         throw new RuntimeException("Failed to find non-null cell");
     }
 
@@ -572,7 +572,7 @@
             cellElement.setClassName("");
         }
     }
-    
+
     public List<Integer> getAllTestIndices() {
         List<Integer> testIndices = new ArrayList<Integer>();
 
diff --git a/frontend/client/src/autotest/common/table/TableRenderer.java b/frontend/client/src/autotest/common/table/TableRenderer.java
index 1641dfb..5384cd3 100644
--- a/frontend/client/src/autotest/common/table/TableRenderer.java
+++ b/frontend/client/src/autotest/common/table/TableRenderer.java
@@ -13,14 +13,14 @@
     // min-width/min-height aren't supported in the hosted mode browser
     public static final String SIZE_PREFIX = GWT.isScript() ? "min-" : "";
     private static final String NONCLICKABLE_CLASS = "spreadsheet-cell-nonclickable";
-    
+
     protected String attributeString(String attribute, String value) {
         if (value.equals(""))
             return "";
         return " " + attribute + "=\"" + value + "\"";
     }
-    
-    public void renderRowsAndAppend(HTMLTable tableObject, CellInfo[][] rows, 
+
+    public void renderRowsAndAppend(HTMLTable tableObject, CellInfo[][] rows,
                                     int startRow, int maxRows, boolean renderNull) {
         StringBuffer htmlBuffer= new StringBuffer();
         htmlBuffer.append("<table><tbody>");
@@ -33,17 +33,16 @@
                     htmlBuffer.append("<td> </td>");
                 } else if (cell != null) {
                     String tdAttributes = "", divAttributes = "", divStyle = "";
-                    if (cell.color != null) {
-                        tdAttributes += attributeString("style", 
-                                                       "background-color: " + cell.color + ";");
+                    if (cell.cssClass != null) {
+                        tdAttributes += attributeString("class", cell.cssClass);
                     }
                     if (cell.rowSpan > 1) {
-                        tdAttributes += attributeString("rowspan", Integer.toString(cell.rowSpan)); 
+                        tdAttributes += attributeString("rowspan", Integer.toString(cell.rowSpan));
                     }
                     if (cell.colSpan > 1) {
-                        tdAttributes += attributeString("colspan", Integer.toString(cell.colSpan)); 
+                        tdAttributes += attributeString("colspan", Integer.toString(cell.colSpan));
                     }
-                    
+
                     if (cell.widthPx != null) {
                         divStyle += SIZE_PREFIX + "width: " + cell.widthPx + "px; ";
                     }
@@ -56,7 +55,7 @@
                     if (cell.isEmpty()) {
                         divAttributes += attributeString("class", NONCLICKABLE_CLASS);
                     }
-                    
+
                     htmlBuffer.append("<td " + tdAttributes + ">");
                     htmlBuffer.append("<div " + divAttributes + ">");
                     htmlBuffer.append(cell.contents);
@@ -66,15 +65,15 @@
             htmlBuffer.append("</tr>");
         }
         htmlBuffer.append("</tbody></table>");
-        
+
         renderBody(tableObject, htmlBuffer.toString());
     }
-    
+
     public void renderRows(HTMLTable tableObject, CellInfo[][] rows, boolean renderNull) {
         DomUtils.clearDomChildren(tableObject.getElement()); // remove existing tbodies
         renderRowsAndAppend(tableObject, rows, 0, rows.length, renderNull);
     }
-    
+
     public void renderRows(HTMLTable tableObject, CellInfo[][] rows) {
         renderRows(tableObject, rows, true);
     }
@@ -83,15 +82,15 @@
         // render the table within a DIV
         Element tempDiv = DOM.createDiv();
         tempDiv.setInnerHTML(html);
-        
+
         // inject the new tbody into the existing table
         Element newTable = tempDiv.getFirstChildElement();
         Element newBody = newTable.getFirstChildElement();
         tableObject.getElement().appendChild(newBody);
-        
+
         setBodyElement(tableObject, newBody);
     }
-    
+
     /**
      * A little hack to set the private member variable bodyElem of an HTMLTable.
      */
diff --git a/frontend/client/src/autotest/planner/machine/MachineViewTable.java b/frontend/client/src/autotest/planner/machine/MachineViewTable.java
index 57bd8a9..b315892 100644
--- a/frontend/client/src/autotest/planner/machine/MachineViewTable.java
+++ b/frontend/client/src/autotest/planner/machine/MachineViewTable.java
@@ -1,5 +1,6 @@
 package autotest.planner.machine;
 
+import autotest.common.AbstractStatusSummary;
 import autotest.common.Utils;
 
 import com.google.gwt.json.client.JSONArray;
@@ -15,49 +16,54 @@
 import java.util.TreeSet;
 
 public class MachineViewTable {
+    public static class RowDisplay {
+        String content;
+        String cssClass = AbstractStatusSummary.BLANK_COLOR;
 
-    public static class PassRate {
-        int passed;
-        int total;
+        public RowDisplay(String content) {
+            this.content = content;
+        }
+
+        public RowDisplay(String content, String cssClass) {
+            this.content = content;
+            this.cssClass = cssClass;
+        }
     }
 
     public static class Row {
         String machine;
         String status;
-        Map<String, PassRate> passRates;
+        Map<String, StatusSummary> statusSummaries;
         List<String> bugIds;
 
         private Row(String machine, String status,
-                Map<String, PassRate> passRates, List<String> bugIds) {
+                Map<String, StatusSummary> statusSummaries, List<String> bugIds) {
             this.machine = machine;
             this.status = status;
-            this.passRates = passRates;
+            this.statusSummaries = statusSummaries;
             this.bugIds = bugIds;
         }
 
         public static Row fromJsonObject(JSONObject rowObject) {
-            Map<String, PassRate> passRates = new HashMap<String, PassRate>();
+            Map<String, StatusSummary> statusSummaries = new HashMap<String, StatusSummary>();
 
             JSONArray testsRun = rowObject.get("tests_run").isArray();
             for (int i = 0; i < testsRun.size(); i++) {
                 JSONObject test = testsRun.get(i).isObject();
                 String testName = Utils.jsonToString(test.get("test_name"));
 
-                PassRate passRate = passRates.get(testName);
-                if (passRate == null) {
-                    passRate = new PassRate();
-                    passRates.put(testName, passRate);
+                StatusSummary statusSummary = statusSummaries.get(testName);
+                if (statusSummary == null) {
+                    statusSummary = new StatusSummary();
+                    statusSummaries.put(testName, statusSummary);
                 }
 
-                passRate.total++;
-                if (test.get("success").isBoolean().booleanValue()) {
-                    passRate.passed++;
-                }
+                statusSummary.addStatus(Utils.jsonToString(test.get("status")));
             }
 
             return new Row(Utils.jsonToString(rowObject.get("machine")),
                     Utils.jsonToString(rowObject.get("status")),
-                    passRates,
+                    statusSummaries,
                     Arrays.asList(Utils.JSONtoStrings(rowObject.get("bug_ids").isArray())));
         }
     }
@@ -65,7 +71,7 @@
     public static interface Display {
         public void clearData();
         public void setHeaders(Collection<String> headers);
-        public void addRow(Collection<String> rowData);
+        public void addRow(Collection<RowDisplay> rowData);
         public void finalRender();
     }
 
@@ -88,7 +94,7 @@
     private void displayData() {
         Set<String> allTestNames = new TreeSet<String>();
         for (Row row : rows) {
-            allTestNames.addAll(row.passRates.keySet());
+            allTestNames.addAll(row.statusSummaries.keySet());
         }
 
         List<String> headers = new ArrayList<String>();
@@ -99,20 +105,21 @@
         display.setHeaders(headers);
 
         for (Row row : rows) {
-            List<String> rowData = new ArrayList<String>();
-            rowData.add(row.machine);
-            rowData.add(row.status);
+            List<RowDisplay> rowData = new ArrayList<RowDisplay>();
+            rowData.add(new RowDisplay(row.machine));
+            rowData.add(new RowDisplay(row.status));
 
             for (String testName : allTestNames) {
-                PassRate passRate = row.passRates.get(testName);
-                if (passRate != null) {
-                    rowData.add(passRate.passed + "/" + passRate.total);
+                StatusSummary statusSummary = row.statusSummaries.get(testName);
+                if (statusSummary != null) {
+                    rowData.add(new RowDisplay(
+                            statusSummary.formatStatusCounts(), statusSummary.getCssClass()));
                 } else {
-                    rowData.add("");
+                    rowData.add(new RowDisplay(""));
                 }
             }
 
-            rowData.add(String.valueOf(row.bugIds.size()));
+            rowData.add(new RowDisplay(String.valueOf(row.bugIds.size())));
 
             display.addRow(rowData);
         }
diff --git a/frontend/client/src/autotest/planner/machine/MachineViewTableDisplay.java b/frontend/client/src/autotest/planner/machine/MachineViewTableDisplay.java
index c970bf1..069fe58 100644
--- a/frontend/client/src/autotest/planner/machine/MachineViewTableDisplay.java
+++ b/frontend/client/src/autotest/planner/machine/MachineViewTableDisplay.java
@@ -1,6 +1,7 @@
 package autotest.planner.machine;
 
 import autotest.planner.TestPlannerUtils;
+import autotest.planner.machine.MachineViewTable.RowDisplay;
 
 import com.google.gwt.event.logical.shared.ResizeEvent;
 import com.google.gwt.event.logical.shared.ResizeHandler;
@@ -26,6 +27,7 @@
         scrollTable.setSortPolicy(SortPolicy.DISABLED);
         scrollTable.setResizePolicy(ResizePolicy.UNCONSTRAINED);
         scrollTable.setScrollPolicy(ScrollPolicy.BOTH);
+        dataTable.setSelectionEnabled(false);
 
         scrollTable.setSize("100%", TestPlannerUtils.getHeightParam(Window.getClientHeight()));
         scrollTable.setVisible(false);
@@ -34,14 +36,15 @@
     }
 
     @Override
-    public void addRow(Collection<String> rowData) {
+    public void addRow(Collection<RowDisplay> rowData) {
         assert rowData.size() == dataTable.getColumnCount();
 
         int row = dataTable.insertRow(dataTable.getRowCount());
 
         int column = 0;
-        for (String data : rowData) {
-            dataTable.setText(row, column++, data);
+        for (RowDisplay data : rowData) {
+            dataTable.setText(row, column, data.content);
+            dataTable.getCellFormatter().addStyleName(row, column++, data.cssClass);
         }
     }
 
diff --git a/frontend/client/src/autotest/planner/machine/StatusSummary.java b/frontend/client/src/autotest/planner/machine/StatusSummary.java
new file mode 100644
index 0000000..59d45ba
--- /dev/null
+++ b/frontend/client/src/autotest/planner/machine/StatusSummary.java
@@ -0,0 +1,36 @@
+package autotest.planner.machine;
+
+import autotest.common.AbstractStatusSummary;
+
+class StatusSummary extends AbstractStatusSummary {
+    static final String GOOD_STATUS = "GOOD";
+    static final String RUNNING_STATUS = "RUNNING";
+
+    private int complete;
+    private int incomplete;
+    private int passed;
+
+    void addStatus(String status) {
+        if (status.equals(GOOD_STATUS)) {
+            complete++;
+            passed++;
+        } else if (status.equals(RUNNING_STATUS)) {
+            incomplete++;
+        } else {
+            complete++;
+        }
+    }
+
+    @Override
+    protected int getComplete() {
+        return complete;
+    }
+    @Override
+    protected int getIncomplete() {
+        return incomplete;
+    }
+    @Override
+    protected int getPassed() {
+        return passed;
+    }
+}
diff --git a/frontend/client/src/autotest/public/common.css b/frontend/client/src/autotest/public/common.css
index 65fe13c..dcbd94a 100644
--- a/frontend/client/src/autotest/public/common.css
+++ b/frontend/client/src/autotest/public/common.css
@@ -35,3 +35,30 @@
   font-family: Courier New, Courier, monospace;
 }
 
+td .status_blank {
+  background-color: #ffffff;
+}
+
+td .status_none {
+  background-color: #d080d0;
+}
+
+td .status_bad {
+  background-color: #ff4040;
+}
+
+td .status_75 {
+  background-color: #ffc040;
+}
+
+td .status_85 {
+  background-color: #ffff00;
+}
+
+td .status_90 {
+  background-color: #c0ff80;
+}
+
+td .status_95 {
+  background-color: #32CD32;
+}
diff --git a/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java b/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java
index bae2f06..66445f8 100644
--- a/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java
+++ b/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java
@@ -22,7 +22,7 @@
     private static final NotifyManager notifyManager = NotifyManager.getInstance();
     private static final int MAX_CELL_COUNT = 500000;
     private static final int ROWS_PROCESSED_PER_ITERATION = 1000;
-    
+
     private Spreadsheet spreadsheet;
     private TestGroupDataSource dataSource;
     private int numTotalTests;
@@ -32,7 +32,7 @@
     private Command onFinished;
     private Duration timer;
     private Query currentQuery;
-    
+
     public static class TooManyCellsError extends Exception {
         public int cellCount;
 
@@ -41,16 +41,16 @@
             this.cellCount = cellCount;
         }
     }
-    
+
     private class ProcessDataCommand implements IncrementalCommand {
         private int state = 0;
         private List<JSONObject> counts;
         private int currentRow = 0;
-        
+
         public ProcessDataCommand(List<JSONObject> counts) {
             this.counts = counts;
         }
-        
+
         public void processSomeRows() {
             for (int i = 0; i < ROWS_PROCESSED_PER_ITERATION; i++, currentRow++) {
                 if (currentRow == counts.size()) {
@@ -60,7 +60,7 @@
                 processRow(counts.get(currentRow).isObject());
             }
         }
-        
+
         public boolean execute() {
             switch (state) {
                 case 0:
@@ -82,7 +82,7 @@
                     processSomeRows();
                     return true;
                 case 3:
-                    // we must make the spreadsheet visible before rendering, or size computations 
+                    // we must make the spreadsheet visible before rendering, or size computations
                     // won't work correctly
                     spreadsheet.setVisible(true);
                     break;
@@ -95,7 +95,7 @@
                     finalizeCommand();
                     return false;
             }
-            
+
             state++;
             return true;
         }
@@ -105,7 +105,7 @@
             onFinished.execute();
         }
     }
-    
+
     public SpreadsheetDataProcessor(Spreadsheet spreadsheet) {
         this.spreadsheet = spreadsheet;
     }
@@ -134,18 +134,18 @@
         StatusSummary statusSummary = StatusSummary.getStatusSummary(group);
         numTotalTests += statusSummary.getTotal();
         cellInfo.contents = statusSummary.formatContents();
-        cellInfo.color = statusSummary.getColor();
+        cellInfo.cssClass = statusSummary.getCssClass();
         cellInfo.testCount = statusSummary.getTotal();
         cellInfo.testIndex = (int) group.get("test_idx").isNumber().doubleValue();
         lastCellInfo = cellInfo;
     }
-    
+
     public void refresh(JSONObject condition, Command onFinished) {
         timer = new Duration();
         this.onFinished = onFinished;
         dataSource.query(condition, this);
     }
-    
+
     public void onQueryReady(Query query) {
         currentQuery = query;
         query.getPage(null, null, null, this);
@@ -172,7 +172,7 @@
     public int getNumTotalTests() {
         return numTotalTests;
     }
-    
+
     /**
      * This is useful when there turns out to be only a single test return.
      * @return the last CellInfo created.  Should only really be called when there was only a single
@@ -182,16 +182,16 @@
         assert numTotalTests == 1;
         return lastCellInfo;
     }
-    
+
     public void onError(JSONObject errorObject) {
         onFinished.execute();
     }
 
-    public void setHeaders(List<HeaderField> rowFields, List<HeaderField> columnFields, 
+    public void setHeaders(List<HeaderField> rowFields, List<HeaderField> columnFields,
                            JSONObject queryParameters) {
         this.rowFields = getHeaderSqlNames(rowFields);
         this.columnFields = getHeaderSqlNames(columnFields);
-        
+
         List<List<String>> headerGroups = new ArrayList<List<String>>();
         headerGroups.add(this.rowFields);
         headerGroups.add(this.columnFields);
@@ -214,7 +214,7 @@
     public TestGroupDataSource getDataSource() {
         return dataSource;
     }
-    
+
     public Query getCurrentQuery() {
         return currentQuery;
     }
diff --git a/frontend/client/src/autotest/tko/StatusSummary.java b/frontend/client/src/autotest/tko/StatusSummary.java
index eba22a0..00602df 100644
--- a/frontend/client/src/autotest/tko/StatusSummary.java
+++ b/frontend/client/src/autotest/tko/StatusSummary.java
@@ -2,105 +2,71 @@
 
 package autotest.tko;
 
+import autotest.common.AbstractStatusSummary;
 import autotest.common.Utils;
 
 import com.google.gwt.json.client.JSONObject;
 
 import java.util.Arrays;
 
-class StatusSummary {
-    static final String BLANK_COLOR = "#FFFFFF";
-    static final ColorMapping[] CELL_COLOR_MAP = {
-        // must be in descending order of percentage
-        new ColorMapping(95, "#32CD32"),
-        new ColorMapping(90, "#c0ff80"),
-        new ColorMapping(85, "#ffff00"),
-        new ColorMapping(75, "#ffc040"),
-        new ColorMapping(1, "#ff4040"),
-        new ColorMapping(0, "#d080d0"),
-    };
-    
+class StatusSummary extends AbstractStatusSummary {
     public int passed = 0;
     public int complete = 0;
     public int incomplete = 0;
     public int total = 0; // TEST_NA is included here, but not in any other
-    
+
     private String[] contents = null;
-    
-    /**
-     * Stores a color for pass rates and the minimum passing percentage required
-     * to have that color.
-     */
-    static class ColorMapping {
-        // store percentage as int so we can reprint it consistently
-        public int minPercent;
-        public String htmlColor;
-        
-        public ColorMapping(int minPercent, String htmlColor) {
-            this.minPercent = minPercent;
-            this.htmlColor = htmlColor;
-        }
-        
-        public boolean matches(double ratio) {
-            return ratio * 100 >= minPercent;
-        }
-    }
-    
+
     public static StatusSummary getStatusSummary(JSONObject group) {
         StatusSummary summary = new StatusSummary();
         summary.passed = getField(group, TestGroupDataSource.PASS_COUNT_FIELD);
         summary.complete = getField(group, TestGroupDataSource.COMPLETE_COUNT_FIELD);
         summary.incomplete = getField(group, TestGroupDataSource.INCOMPLETE_COUNT_FIELD);
         summary.total = getField(group, TestGroupDataSource.GROUP_COUNT_FIELD);
-        
+
         if (group.containsKey("extra_info")) {
             summary.contents = Utils.JSONtoStrings(group.get("extra_info").isArray());
         }
-        
+
         return summary;
     }
 
     private static int getField(JSONObject group, String field) {
         return (int) group.get(field).isNumber().doubleValue();
     }
-    
+
     /**
      * Force construction to go through getStatusSummary() factory method.
      */
     private StatusSummary() {}
-    
+
     public int getTotal() {
         return total;
     }
 
     public String formatContents() {
         String result = formatStatusCounts();
-        
+
         if (contents != null) {
             result += "<br>";
             result += Utils.joinStrings("<br>", Arrays.asList(contents), true);
         }
-        
+
         return result;
     }
-    
-    private String formatStatusCounts() {
-        String text = passed + " / " + complete;
-        if (incomplete > 0) {
-            text += " (" + incomplete + " incomplete)";
-        }
-        return text;
+
+    @Override
+    protected int getComplete() {
+        return complete;
     }
 
-    public String getColor() {
-        if (complete == 0) {
-            return BLANK_COLOR;
-        }
-        double ratio = (double) passed / complete;
-        for (ColorMapping mapping : CELL_COLOR_MAP) {
-            if (mapping.matches(ratio))
-                return mapping.htmlColor;
-        }
-        throw new RuntimeException("No color map match for ratio " + ratio);
+    @Override
+    protected int getIncomplete() {
+        return incomplete;
+    }
+
+    @Override
+    protected int getPassed() {
+        return passed;
     }
 }
\ No newline at end of file
diff --git a/frontend/client/src/autotest/tko/TableView.java b/frontend/client/src/autotest/tko/TableView.java
index ae29e67..c3d04b0 100644
--- a/frontend/client/src/autotest/tko/TableView.java
+++ b/frontend/client/src/autotest/tko/TableView.java
@@ -43,14 +43,14 @@
 import java.util.ListIterator;
 import java.util.Map;
 
-public class TableView extends ConditionTabView 
-                       implements DynamicTableListener, TableActionsWithExportCsvListener, 
-                                  ClickHandler, TableWidgetFactory, CommonPanelListener, 
+public class TableView extends ConditionTabView
+                       implements DynamicTableListener, TableActionsWithExportCsvListener,
+                                  ClickHandler, TableWidgetFactory, CommonPanelListener,
                                   MultiListSelectPresenter.GeneratorHandler {
     private static final int ROWS_PER_PAGE = 30;
     private static final String COUNT_NAME = "Count in group";
     private static final String STATUS_COUNTS_NAME = "Test pass rate";
-    private static final String[] DEFAULT_COLUMNS = 
+    private static final String[] DEFAULT_COLUMNS =
         {"Test index", "Test name", "Job tag", "Hostname", "Status"};
     private static final String[] TRIAGE_GROUP_COLUMNS =
         {"Test name", "Status", COUNT_NAME, "Reason"};
@@ -87,14 +87,14 @@
             return false;
         }
     }
-    
+
     private GroupCountField groupCountField =
         new GroupCountField(COUNT_NAME, TestGroupDataSource.GROUP_COUNT_FIELD);
     private GroupCountField statusCountsField =
         new GroupCountField(STATUS_COUNTS_NAME, DataTable.WIDGET_COLUMN);
 
     private TestSelectionListener listener;
-    
+
     private DynamicTable table;
     private TableDecorator tableDecorator;
     private SelectionManager selectionManager;
@@ -107,7 +107,7 @@
 
     private DoubleListSelector columnSelectDisplay = new DoubleListSelector();
     private CheckBox groupCheckbox = new CheckBox("Group by these columns and show counts");
-    private CheckBox statusGroupCheckbox = 
+    private CheckBox statusGroupCheckbox =
         new CheckBox("Group by these columns and show pass rates");
     private Button queryButton = new Button("Query");
     private Panel tablePanel = new SimplePanel();
@@ -117,11 +117,11 @@
     public enum TableViewConfig {
         DEFAULT, PASS_RATE, TRIAGE
     }
-    
+
     public static interface TableSwitchListener extends TestSelectionListener {
         public void onSwitchToTable(TableViewConfig config);
     }
-    
+
     public TableView(TestSelectionListener listener) {
         this.listener = listener;
         commonPanel.addListener(this);
@@ -147,17 +147,17 @@
         queryButton.addClickHandler(this);
         groupCheckbox.addClickHandler(this);
         statusGroupCheckbox.addClickHandler(this);
-        
+
         Panel columnPanel = new VerticalPanel();
         columnPanel.add(columnSelectDisplay);
         columnPanel.add(groupCheckbox);
         columnPanel.add(statusGroupCheckbox);
-        
+
         addWidget(columnPanel, "table_column_select");
         addWidget(queryButton, "table_query_controls");
         addWidget(tablePanel, "table_table");
     }
-    
+
     private void selectColumnsByName(String[] columnNames) {
         List<HeaderField> fields = new ArrayList<HeaderField>();
         for (String name : columnNames) {
@@ -166,7 +166,7 @@
         columnSelect.setSelectedItems(fields);
         cleanupSortsForNewColumns();
     }
-    
+
     public void setupDefaultView() {
         tableSorts.clear();
         selectColumnsByName(DEFAULT_COLUMNS);
@@ -198,14 +198,14 @@
         table.addListener(this);
         table.setWidgetFactory(this);
         restoreTableSorting();
-        
+
         tableDecorator = new TableDecorator(table);
         tableDecorator.addPaginators();
         selectionManager = tableDecorator.addSelectionManager(false);
         tableDecorator.addTableActionsWithExportCsvListener(this);
         tablePanel.clear();
         tablePanel.add(tableDecorator);
-        
+
         selectionManager = new SelectionManager(table, false);
     }
 
@@ -234,7 +234,7 @@
         } else {
             groupDataSource = TestGroupDataSource.getStatusCountDataSource();
         }
-        
+
         updateGroupColumns();
         return groupDataSource;
     }
@@ -243,7 +243,7 @@
         commonPanel.updateStateFromView();
         columnSelect.updateStateFromView();
     }
-    
+
     private void updateViewFromState() {
         commonPanel.updateViewFromState();
         columnSelect.updateViewFromState();
@@ -263,7 +263,7 @@
     private boolean isGroupField(HeaderField field) {
         return field instanceof GroupCountField;
     }
-    
+
     private void saveTableSorting() {
         if (table != null) {
             // we need our own copy so we can modify it later
@@ -272,7 +272,7 @@
     }
 
     private void restoreTableSorting() {
-        for (ListIterator<SortSpec> i = tableSorts.listIterator(tableSorts.size()); 
+        for (ListIterator<SortSpec> i = tableSorts.listIterator(tableSorts.size());
              i.hasPrevious();) {
             SortSpec sortSpec = i.previous();
             table.sortOnColumn(sortSpec.getField(), sortSpec.getDirection());
@@ -313,7 +313,7 @@
         sqlConditionFilter.setAllParameters(condition);
         table.refresh();
     }
-    
+
     @Override
     public void doQuery() {
         if (savedColumns().isEmpty()) {
@@ -323,7 +323,7 @@
         updateStateFromView();
         refresh();
     }
-    
+
     @Override
     public void onRowClicked(int rowIndex, JSONObject row, boolean isRightClick) {
         Event event = Event.getCurrentEvent();
@@ -336,7 +336,7 @@
             menu.showAtWindow(event.getClientX(), event.getClientY());
             return;
         }
-        
+
         if (isSelectEvent(event)) {
             selectionManager.toggleSelected(row);
             return;
@@ -353,7 +353,7 @@
 
     private ContextMenu getContextMenu(final TestSet testSet) {
         TestContextMenu menu = new TestContextMenu(testSet, listener);
-        
+
         if (!menu.addViewDetailsIfSingleTest() && isAnyGroupingEnabled()) {
             menu.addItem("Drill down", new Command() {
                 public void execute() {
@@ -361,7 +361,7 @@
                 }
             });
         }
-        
+
         menu.addLabelItems();
         return menu;
     }
@@ -374,7 +374,7 @@
         restoreHistoryState();
         return historyToken;
     }
-    
+
     private void doDrilldown(TestSet testSet) {
         History.newItem(getDrilldownHistoryToken(testSet).toString());
     }
@@ -395,7 +395,7 @@
         }
         return testSet;
     }
-    
+
     private TestSet getTestSet(Collection<JSONObject> selectedObjects) {
         CompositeTestSet compositeSet = new CompositeTestSet();
         for (JSONObject row : selectedObjects) {
@@ -409,7 +409,7 @@
         saveTableSorting();
         updateHistory();
     }
-    
+
     private void setCheckboxesEnabled() {
         assert !(groupCheckbox.getValue() && statusGroupCheckbox.getValue());
 
@@ -485,7 +485,7 @@
     protected void fillDefaultHistoryValues(Map<String, String> arguments) {
         HeaderField defaultSortField = headerFields.getFieldByName(DEFAULT_COLUMNS[0]);
         Utils.setDefaultValue(arguments, "sort", defaultSortField.getSqlName());
-        Utils.setDefaultValue(arguments, "columns", 
+        Utils.setDefaultValue(arguments, "columns",
                         Utils.joinStrings(",", Arrays.asList(DEFAULT_COLUMNS)));
     }
 
@@ -515,7 +515,7 @@
     private boolean isAnyGroupingEnabled() {
         return getActiveGrouping() != GroupingType.NO_GROUPING;
     }
-    
+
     private GroupingType getGroupingFromFields(List<HeaderField> fields) {
         for (HeaderField field : fields) {
             if (field.getName().equals(COUNT_NAME)) {
@@ -527,7 +527,7 @@
         }
         return GroupingType.NO_GROUPING;
     }
-    
+
     /**
      * Get grouping currently active for displayed table.
      */
@@ -540,8 +540,7 @@
         StatusSummary statusSummary = StatusSummary.getStatusSummary(rowObject);
         SimplePanel panel = new SimplePanel();
         panel.add(new HTML(statusSummary.formatContents()));
-        panel.getElement().getStyle().setProperty("backgroundColor", 
-                                                  statusSummary.getColor());
+        panel.getElement().addClassName(statusSummary.getCssClass());
         return panel;
     }
 
@@ -563,10 +562,10 @@
     public void onExportCsv() {
         JSONObject extraParams = new JSONObject();
         extraParams.put("columns", buildCsvColumnSpecs());
-        TkoUtils.doCsvRequest((RpcDataSource) table.getDataSource(), table.getCurrentQuery(), 
+        TkoUtils.doCsvRequest((RpcDataSource) table.getDataSource(), table.getCurrentQuery(),
                               extraParams);
     }
-    
+
     private JSONArray buildCsvColumnSpecs() {
         String[][] columnSpecs = buildColumnSpecs();
         JSONArray jsonColumnSpecs = new JSONArray();
diff --git a/frontend/planner/rpc_interface.py b/frontend/planner/rpc_interface.py
index cc4eb89..c6b0e23 100644
--- a/frontend/planner/rpc_interface.py
+++ b/frontend/planner/rpc_interface.py
@@ -466,23 +466,23 @@
         tests_run = []
 
         machine = host.host.hostname
-        status = host.status()
+        host_status = host.status()
         bug_ids = set()
 
         testruns = plan.testrun_set.filter(host=host, invalidated=False,
                                            finalized=True)
         for testrun in testruns:
             test_name = testrun.tko_test.test
-            success = (testrun.tko_test.status.word == 'GOOD')
+            test_status = testrun.tko_test.status.word
             testrun_bug_ids = testrun.bugs.all().values_list(
                     'external_uid', flat=True)
 
             tests_run.append({'test_name': test_name,
-                              'success': success})
+                              'status': test_status})
             bug_ids.update(testrun_bug_ids)
 
         result.append({'machine': machine,
-                       'status': status,
+                       'status': host_status,
                        'tests_run': tests_run,
                        'bug_ids': list(bug_ids)})
     return result
diff --git a/frontend/planner/rpc_interface_unittest.py b/frontend/planner/rpc_interface_unittest.py
index 9dd345b..0048307 100644
--- a/frontend/planner/rpc_interface_unittest.py
+++ b/frontend/planner/rpc_interface_unittest.py
@@ -215,7 +215,7 @@
                                                 finalized=True)
 
         host1_expected['tests_run'] = [{'test_name': 'test',
-                                        'success': False}]
+                                        'status': self._running_status.word}]
         actual = rpc_interface.get_machine_view_data(plan_id=self._plan.id)
         self.assertEqual(sorted(actual), sorted(expected))
 
@@ -226,7 +226,7 @@
         testrun.bugs.add(bug)
 
         host1_expected['tests_run'] = [{'test_name': 'test',
-                                        'success': True}]
+                                        'status': self._good_status.word}]
         host1_expected['bug_ids'] = ['bug']
         actual = rpc_interface.get_machine_view_data(plan_id=self._plan.id)
         self.assertEqual(sorted(actual), sorted(expected))