Initial implementation of Test Planner Overview tab

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



git-svn-id: http://test.kernel.org/svn/autotest/trunk@4503 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/client/src/autotest/TestPlannerClient.gwt.xml b/frontend/client/src/autotest/TestPlannerClient.gwt.xml
index 0650326..f726a8b 100644
--- a/frontend/client/src/autotest/TestPlannerClient.gwt.xml
+++ b/frontend/client/src/autotest/TestPlannerClient.gwt.xml
@@ -15,4 +15,5 @@
   <stylesheet src='scrolltable.css'/>
   <stylesheet src='afeclient.css'/>
   <stylesheet src='tkoclient.css'/>
+  <stylesheet src='testplannerclient.css'/>
 </module>
diff --git a/frontend/client/src/autotest/common/JsonManipulator.java b/frontend/client/src/autotest/common/JsonManipulator.java
new file mode 100644
index 0000000..8dfdd86
--- /dev/null
+++ b/frontend/client/src/autotest/common/JsonManipulator.java
@@ -0,0 +1,21 @@
+package autotest.common;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class JsonManipulator {
+    public static interface IFactory<T> {
+        public T fromJsonObject(JSONObject object);
+    }
+
+    public static <T> List<T> createList(JSONArray objects, IFactory<T> factory) {
+        List<T> list = new ArrayList<T>();
+        for (JSONObject object : new JSONArrayList<JSONObject>(objects)) {
+            list.add(factory.fromJsonObject(object));
+        }
+        return list;
+    }
+}
diff --git a/frontend/client/src/autotest/planner/OverviewTab.java b/frontend/client/src/autotest/planner/OverviewTab.java
deleted file mode 100644
index a8054f0..0000000
--- a/frontend/client/src/autotest/planner/OverviewTab.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package autotest.planner;
-
-
-public class OverviewTab {
-  
-    public static interface Display {
-        // Not yet implemented
-    }
-    
-    @SuppressWarnings("unused")
-    private Display display;
-    
-    public void bindDisplay(Display display) {
-        this.display = display;
-    }
-    
-}
diff --git a/frontend/client/src/autotest/planner/OverviewTabDisplay.java b/frontend/client/src/autotest/planner/OverviewTabDisplay.java
deleted file mode 100644
index ac2841c..0000000
--- a/frontend/client/src/autotest/planner/OverviewTabDisplay.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package autotest.planner;
-
-import autotest.common.ui.TabView;
-
-
-public class OverviewTabDisplay extends TabView implements OverviewTab.Display {
-  
-    @Override
-    public String getElementId() {
-        return "overview";
-    }
-    
-}
diff --git a/frontend/client/src/autotest/planner/TestPlanSelector.java b/frontend/client/src/autotest/planner/TestPlanSelector.java
index ff1590e..0e38c3c 100644
--- a/frontend/client/src/autotest/planner/TestPlanSelector.java
+++ b/frontend/client/src/autotest/planner/TestPlanSelector.java
@@ -13,52 +13,57 @@
 import java.util.List;
 
 public class TestPlanSelector implements ClickHandler, KeyPressHandler {
-    
+
     public static interface Display {
         public HasText getInputText();
         public HasClickHandlers getShowButton();
         public HasKeyPressHandlers getInputField();
+        public void setVisible(boolean visible);
     }
-    
+
     public static interface Listener {
         public void onPlanSelected();
     }
-    
-    
+
+
     private Display display;
     private String selectedPlan;
     private List<Listener> listeners = new ArrayList<Listener>();
-    
+
     public void bindDisplay(Display display) {
         this.display = display;
         display.getShowButton().addClickHandler(this);
         display.getInputField().addKeyPressHandler(this);
     }
-    
+
     public void addListener(Listener listener) {
         listeners.add(listener);
     }
-    
+
     @Override
     public void onClick(ClickEvent event) {
         selectPlan();
     }
-    
+
     @Override
     public void onKeyPress(KeyPressEvent event) {
         if (event.getCharCode() == KeyCodes.KEY_ENTER) {
             selectPlan();
         }
     }
-    
+
     private void selectPlan() {
         selectedPlan = display.getInputText().getText();
         for (Listener listener : listeners) {
             listener.onPlanSelected();
         }
     }
-    
+
     public String getSelectedPlan() {
         return selectedPlan;
     }
+
+    public void setVisible(boolean visible) {
+        display.setVisible(visible);
+    }
 }
diff --git a/frontend/client/src/autotest/planner/TestPlannerClient.java b/frontend/client/src/autotest/planner/TestPlannerClient.java
index 5b857b7..4a60e76 100644
--- a/frontend/client/src/autotest/planner/TestPlannerClient.java
+++ b/frontend/client/src/autotest/planner/TestPlannerClient.java
@@ -7,6 +7,7 @@
 import autotest.common.ui.CustomTabPanel;
 import autotest.common.ui.NotifyManager;
 import autotest.planner.machine.MachineViewTab;
+import autotest.planner.overview.OverviewTab;
 import autotest.planner.triage.TriageViewTab;
 
 import com.google.gwt.core.client.EntryPoint;
@@ -17,16 +18,13 @@
     private TestPlanSelector planSelector = new TestPlanSelector();
     private TestPlanSelectorDisplay planSelectorView = new TestPlanSelectorDisplay();
 
-    private OverviewTab overviewTab = new OverviewTab();
-    private OverviewTabDisplay overviewTabDisplay = new OverviewTabDisplay();
-
+    private OverviewTab overviewTab = new OverviewTab(planSelector);
     private MachineViewTab machineViewTab = new MachineViewTab(planSelector);
+    private TriageViewTab triageViewTab = new TriageViewTab(planSelector);
 
     private TestViewTab testViewTab = new TestViewTab();
     private TestViewTabDisplay testViewTabDisplay = new TestViewTabDisplay();
 
-    private TriageViewTab triageViewTab = new TriageViewTab(planSelector);
-
     private AutoprocessedTab autoprocessedTab = new AutoprocessedTab();
     private AutoprocessedTabDisplay autoprocessedTabDisplay = new AutoprocessedTabDisplay();
 
@@ -51,7 +49,6 @@
     private void finishLoading() {
         SiteCommonClassFactory.globalInitialize();
 
-        overviewTab.bindDisplay(overviewTabDisplay);
         testViewTab.bindDisplay(testViewTabDisplay);
         autoprocessedTab.bindDisplay(autoprocessedTabDisplay);
         historyTab.bindDisplay(historyTabDisplay);
@@ -60,7 +57,7 @@
         planSelector.bindDisplay(planSelectorView);
         mainTabPanel.getCommonAreaPanel().add(planSelectorView);
 
-        mainTabPanel.addTabView(overviewTabDisplay);
+        mainTabPanel.addTabView(overviewTab);
         mainTabPanel.addTabView(machineViewTab);
         mainTabPanel.addTabView(testViewTabDisplay);
         mainTabPanel.addTabView(triageViewTab);
diff --git a/frontend/client/src/autotest/planner/TestPlannerTab.java b/frontend/client/src/autotest/planner/TestPlannerTab.java
index 0e60993..0d8b7f8 100644
--- a/frontend/client/src/autotest/planner/TestPlannerTab.java
+++ b/frontend/client/src/autotest/planner/TestPlannerTab.java
@@ -26,6 +26,16 @@
         getPresenter().refresh();
     }
 
+    @Override
+    public void display() {
+        super.display();
+        setSelectorVisible(true);
+    }
+
+    protected final void setSelectorVisible(boolean visible) {
+        selector.setVisible(visible);
+    }
+
     protected abstract TestPlannerPresenter getPresenter();
     protected abstract TestPlannerDisplay getDisplay();
     protected abstract void bindDisplay();
diff --git a/frontend/client/src/autotest/planner/overview/OverviewTab.java b/frontend/client/src/autotest/planner/overview/OverviewTab.java
new file mode 100644
index 0000000..7a4f5f6
--- /dev/null
+++ b/frontend/client/src/autotest/planner/overview/OverviewTab.java
@@ -0,0 +1,41 @@
+package autotest.planner.overview;
+
+import autotest.planner.TestPlanSelector;
+import autotest.planner.TestPlannerDisplay;
+import autotest.planner.TestPlannerPresenter;
+import autotest.planner.TestPlannerTab;
+
+public class OverviewTab extends TestPlannerTab {
+    private OverviewTabPresenter presenter = new OverviewTabPresenter();
+    private OverviewTabDisplay display = new OverviewTabDisplay();
+
+    public OverviewTab(TestPlanSelector selector) {
+        super(selector);
+    }
+
+    @Override
+    public String getElementId() {
+        return "overview";
+    }
+
+    @Override
+    protected TestPlannerDisplay getDisplay() {
+        return display;
+    }
+
+    @Override
+    protected TestPlannerPresenter getPresenter() {
+        return presenter;
+    }
+
+    @Override
+    protected void bindDisplay() {
+        presenter.bindDisplay(display);
+    }
+
+    @Override
+    public void display() {
+        super.display();
+        setSelectorVisible(false);
+    }
+}
diff --git a/frontend/client/src/autotest/planner/overview/OverviewTabDisplay.java b/frontend/client/src/autotest/planner/overview/OverviewTabDisplay.java
new file mode 100644
index 0000000..1b09fc5
--- /dev/null
+++ b/frontend/client/src/autotest/planner/overview/OverviewTabDisplay.java
@@ -0,0 +1,64 @@
+package autotest.planner.overview;
+
+import autotest.common.ui.NotifyManager;
+import autotest.planner.TestPlannerDisplay;
+
+import com.google.gwt.event.dom.client.HasClickHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.HasText;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.TextBox;
+
+public class OverviewTabDisplay implements TestPlannerDisplay, OverviewTabPresenter.Display {
+    Label addPlanLabel = new Label("Add test plan:");
+    TextBox addPlanField = new TextBox();
+    Button addPlanButton = new Button("add");
+    Panel overviewTablePanel = new SimplePanel();
+
+    @Override
+    public void initialize(HTMLPanel htmlPanel) {
+        HorizontalPanel addPlanContainer = new HorizontalPanel();
+        addPlanContainer.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
+        addPlanContainer.setSpacing(2);
+        addPlanContainer.add(addPlanLabel);
+        addPlanContainer.add(addPlanField);
+        addPlanContainer.add(addPlanButton);
+
+        htmlPanel.add(addPlanContainer, "overview_add_plan");
+        htmlPanel.add(overviewTablePanel, "overview_table");
+    }
+
+    @Override
+    public HasClickHandlers getAddPlanButton() {
+        return addPlanButton;
+    }
+
+    @Override
+    public HasKeyPressHandlers getAddPlanField() {
+        return addPlanField;
+    }
+
+    @Override
+    public HasText getAddPlanText() {
+        return addPlanField;
+    }
+
+    @Override
+    public OverviewTable.Display generateOverviewTableDisplay() {
+        OverviewTableDisplay display = new OverviewTableDisplay();
+        overviewTablePanel.clear();
+        overviewTablePanel.add(display);
+        return display;
+    }
+
+    @Override
+    public void setLoading(boolean loading) {
+        NotifyManager.getInstance().setLoading(loading);
+    }
+}
diff --git a/frontend/client/src/autotest/planner/overview/OverviewTabPresenter.java b/frontend/client/src/autotest/planner/overview/OverviewTabPresenter.java
new file mode 100644
index 0000000..214f02d
--- /dev/null
+++ b/frontend/client/src/autotest/planner/overview/OverviewTabPresenter.java
@@ -0,0 +1,119 @@
+package autotest.planner.overview;
+
+import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.Utils;
+import autotest.common.ui.HasTabVisible;
+import autotest.planner.TestPlanSelector;
+import autotest.planner.TestPlannerPresenter;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.HasClickHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.ui.HasText;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class OverviewTabPresenter extends TestPlannerPresenter
+        implements ClickHandler, KeyPressHandler {
+
+    public static interface Display {
+        public HasText getAddPlanText();
+        public HasClickHandlers getAddPlanButton();
+        public HasKeyPressHandlers getAddPlanField();
+        public void setLoading(boolean loading);
+        public OverviewTable.Display generateOverviewTableDisplay();
+    }
+
+    private Display display;
+    private Set<String> selectedPlans = new LinkedHashSet<String>();
+    private String lastAdded;
+
+    public void bindDisplay(Display display) {
+        this.display = display;
+        display.getAddPlanButton().addClickHandler(this);
+        display.getAddPlanField().addKeyPressHandler(this);
+    }
+
+    @Override
+    public void initialize(TestPlanSelector selector_unused, HasTabVisible tab_unused) {}
+
+    @Override
+    public void refresh(String planId) {
+        throw new UnsupportedOperationException("Should never be called");
+    }
+
+    @Override
+    public void refresh() {
+        JSONObject params = new JSONObject();
+        JSONArray planIds = new JSONArray();
+        for (String plan : selectedPlans) {
+            planIds.set(planIds.size(), new JSONString(plan));
+        }
+        params.put("plan_ids", planIds);
+
+        display.setLoading(true);
+
+        JsonRpcProxy.getProxy().rpcCall("get_overview_data", params, new JsonRpcCallback() {
+            @Override
+            public void onSuccess(JSONValue result) {
+                OverviewTable table = new OverviewTable();
+                table.bindDisplay(display.generateOverviewTableDisplay());
+                display.setLoading(false);
+                table.setData(prepareData(result.isObject()));
+            }
+
+            @Override
+            public void onError(JSONObject errorObject) {
+                super.onError(errorObject);
+                display.setLoading(false);
+                if (Utils.jsonToString(errorObject.get("name")).equals("DoesNotExist")
+                        && lastAdded != null) {
+                    selectedPlans.remove(lastAdded);
+                    lastAdded = null;
+                    refresh();
+                }
+            }
+        });
+    }
+
+    @Override
+    public void onClick(ClickEvent event) {
+        assert event.getSource() == display.getAddPlanButton();
+        addSelectedPlan();
+    }
+
+    @Override
+    public void onKeyPress(KeyPressEvent event) {
+        assert event.getSource() == display.getAddPlanField();
+        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+            addSelectedPlan();
+        }
+    }
+
+    private void addSelectedPlan() {
+        String plan = display.getAddPlanText().getText();
+        selectedPlans.add(plan);
+        lastAdded = plan;
+        refresh();
+    }
+
+    private Map<String, JSONObject> prepareData(JSONObject data) {
+        Map<String, JSONObject> preparedData = new LinkedHashMap<String, JSONObject>();
+        for (String plan : selectedPlans) {
+            preparedData.put(plan, data.get(plan).isObject());
+        }
+        return preparedData;
+    }
+}
diff --git a/frontend/client/src/autotest/planner/overview/OverviewTable.java b/frontend/client/src/autotest/planner/overview/OverviewTable.java
new file mode 100644
index 0000000..a7c2b8e
--- /dev/null
+++ b/frontend/client/src/autotest/planner/overview/OverviewTable.java
@@ -0,0 +1,204 @@
+package autotest.planner.overview;
+
+import autotest.common.JSONArrayList;
+import autotest.common.JsonManipulator;
+import autotest.common.StaticDataRepository;
+import autotest.common.Utils;
+import autotest.common.JsonManipulator.IFactory;
+
+import com.google.gwt.i18n.client.NumberFormat;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OverviewTable {
+    public static interface Display {
+        public void addHeaderGroup(String group, String[] headers);
+        public void clearAllData();
+        public void addData(String header, List<String> data);
+    }
+
+    private static class Machine {
+        static IFactory<Machine> FACTORY = new IFactory<Machine>() {
+            public Machine fromJsonObject(JSONObject object) {
+                return Machine.fromJsonObject(object);
+            }
+        };
+
+        @SuppressWarnings("unused")     // Will be needed later for drilldowns
+        String hostname;
+
+        Boolean passed;
+        String status;
+
+        private Machine(String hostname, Boolean passed, String status) {
+            this.hostname = hostname;
+            this.passed = passed;
+            this.status = status;
+        }
+
+        private static Machine fromJsonObject(JSONObject machine) {
+            Boolean passed = null;
+            JSONValue passedJson = machine.get("passed");
+            if (passedJson.isNull() == null) {
+                passed = passedJson.isBoolean().booleanValue();
+            }
+
+            return new Machine(Utils.jsonToString(machine.get("hostname")),
+                    passed,
+                    Utils.jsonToString(machine.get("status")));
+        }
+    }
+
+    private static class TestConfig {
+        static IFactory<TestConfig> FACTORY = new IFactory<TestConfig>() {
+            public TestConfig fromJsonObject(JSONObject object) {
+                return TestConfig.fromJsonObject(object);
+            }
+        };
+
+        int complete;
+        int estimatedRuntime;
+
+        private TestConfig(int complete, int estimatedRuntime) {
+            this.complete = complete;
+            this.estimatedRuntime = estimatedRuntime;
+        }
+
+        private static TestConfig fromJsonObject(JSONObject testConfig) {
+            return new TestConfig((int) testConfig.get("complete").isNumber().doubleValue(),
+                    (int) testConfig.get("estimated_runtime").isNumber().doubleValue());
+        }
+    }
+
+    private static class PlanData {
+        List<Machine> machines;
+        List<String> bugs;
+        List<TestConfig> testConfigs;
+
+        private PlanData(List<Machine> machines, List<String> bugs, List<TestConfig> testConfigs) {
+            this.machines = machines;
+            this.bugs = bugs;
+            this.testConfigs = testConfigs;
+        }
+
+        static PlanData fromJsonObject(JSONObject planData) {
+            JSONArray machinesJson = planData.get("machines").isArray();
+            List<Machine> machines = JsonManipulator.createList(machinesJson, Machine.FACTORY);
+
+            JSONArray bugsJson = planData.get("bugs").isArray();
+            List<String> bugs = Arrays.asList(Utils.JSONtoStrings(bugsJson));
+
+            JSONArray testConfigsJson = planData.get("test_configs").isArray();
+            List<TestConfig> testConfigs =
+                    JsonManipulator.createList(testConfigsJson, TestConfig.FACTORY);
+
+            return new PlanData(machines, bugs, testConfigs);
+        }
+    }
+
+    private Display display;
+    private List<String> statuses = new ArrayList<String>();
+
+    public void bindDisplay(Display display) {
+        this.display = display;
+        setupDisplay();
+    }
+
+    private void setupDisplay() {
+        List<String> hostStatuses = new ArrayList<String>();
+        JSONArrayList<JSONString> hostStatusesArray = new JSONArrayList<JSONString>(
+                StaticDataRepository.getRepository().getData("host_statuses").isArray());
+        for (JSONString status : hostStatusesArray) {
+            String statusStr = Utils.jsonToString(status);
+            hostStatuses.add(statusStr);
+            statuses.add(statusStr);
+        }
+
+        display.addHeaderGroup("Basic info",
+                new String[] {"Total machines", "Progress", "Bugs filed"});
+        display.addHeaderGroup("Machine status",
+                hostStatuses.toArray(new String[hostStatuses.size()]));
+        display.addHeaderGroup("Pass rate",
+                new String[] {"Passed", "Failed"});
+        display.addHeaderGroup("Plan statistics",
+                new String[] {"Tests run", "Tests remaining"});
+    }
+
+    public void setData(Map<String, JSONObject> data) {
+        display.clearAllData();
+        for (Map.Entry<String, JSONObject> entry : data.entrySet()) {
+            PlanData planData = PlanData.fromJsonObject(entry.getValue());
+
+            int total = planData.machines.size();
+            String totalStr = String.valueOf(total);
+
+            int runNumber = 0;
+            int runHours = 0;
+            int remainingNumber = 0;
+            int remainingHours = 0;
+
+            for (TestConfig config : planData.testConfigs) {
+                int remaining = total - config.complete;
+
+                runNumber += config.complete;
+                remainingNumber += remaining;
+                runHours += config.complete * config.estimatedRuntime;
+                remainingHours += remaining * config.estimatedRuntime;
+            }
+
+            String progress = percentage(runHours, runHours + remainingHours);
+            String bugs = String.valueOf(planData.bugs.size());
+
+            int passed = 0;
+            int passFailTotal = 0;
+            Map<String, Integer> statusCounts = new HashMap<String, Integer>();
+            for (String status : statuses) {
+                statusCounts.put(status, 0);
+            }
+            for (Machine machine : planData.machines) {
+                statusCounts.put(machine.status, statusCounts.get(machine.status) + 1);
+
+                if (machine.passed != null) {
+                    passFailTotal++;
+                    if (machine.passed) {
+                        passed++;
+                    }
+                }
+            }
+
+            List<String> displayData = new ArrayList<String>();
+            displayData.add(totalStr);
+            displayData.add(progress);
+            displayData.add(bugs);
+            for (String status : statuses) {
+                displayData.add(numberAndPercentage(statusCounts.get(status), total));
+            }
+            displayData.add(numberAndPercentage(passed, passFailTotal));
+            displayData.add(numberAndPercentage(passFailTotal - passed, passFailTotal));
+            displayData.add(runNumber + " (" + runHours + " hours)");
+            displayData.add(remainingNumber + " (" + remainingHours + " hours)");
+
+            display.addData(entry.getKey(), displayData);
+        }
+    }
+
+    private String percentage(int num, int total) {
+        if (total == 0) {
+            return "N/A";
+        }
+        NumberFormat format = NumberFormat.getFormat("##0.#%");
+        return format.format(num / (double) total);
+    }
+
+    private String numberAndPercentage(int num, int total) {
+        return num + " (" + percentage(num, total) + ")";
+    }
+}
diff --git a/frontend/client/src/autotest/planner/overview/OverviewTableDisplay.java b/frontend/client/src/autotest/planner/overview/OverviewTableDisplay.java
new file mode 100644
index 0000000..35436bf
--- /dev/null
+++ b/frontend/client/src/autotest/planner/overview/OverviewTableDisplay.java
@@ -0,0 +1,74 @@
+package autotest.planner.overview;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlexTable;
+
+import java.util.List;
+
+public class OverviewTableDisplay extends Composite implements OverviewTable.Display {
+
+    private FlexTable table = new FlexTable();
+
+    public OverviewTableDisplay() {
+        table.setStyleName("overview_table");
+
+        table.setText(0, 0, "");
+        table.setText(0, 1, "");
+
+        table.getRowFormatter().addStyleName(0, "header");
+        table.getColumnFormatter().addStyleName(0, "header");
+        table.getColumnFormatter().addStyleName(1, "header");
+
+        initWidget(table);
+
+        setVisible(false);
+    }
+
+    @Override
+    public void addData(String header, List<String> data) {
+        assert table.getRowCount() == data.size() + 1;
+
+        setVisible(true);
+
+        setData(0, header);
+        for (int i = 0; i < data.size(); i++) {
+            setData(i + 1, data.get(i));
+        }
+    }
+
+    private void setData(int row, String data) {
+        int cell = table.getCellCount(row);
+        table.setText(row, cell, data);
+        if (!table.getCellFormatter().getStyleName(row, cell).equals("header") &&
+                row > 0 && row % 2 == 0) {
+            table.getCellFormatter().addStyleName(row, cell, "even");
+        }
+    }
+
+    @Override
+    public void addHeaderGroup(String group, String[] headers) {
+        int row = table.getRowCount();
+
+        table.setText(row, 0, group);
+        table.getFlexCellFormatter().setRowSpan(row, 0, headers.length);
+
+        for (int i = 0; i < headers.length; i++) {
+            int cell = 0;
+            if (i == 0) {
+                cell = 1;
+            }
+            table.setText(row + i, cell, headers[i]);
+        }
+    }
+
+    @Override
+    public void clearAllData() {
+        for (int row = 0; row < table.getRowCount(); row++) {
+            for (int col = 2; col < table.getCellCount(row); col++) {
+                table.removeCell(row, col);
+            }
+        }
+        setVisible(false);
+    }
+
+}
diff --git a/frontend/client/src/autotest/public/TestPlannerClient.html b/frontend/client/src/autotest/public/TestPlannerClient.html
index 2fbb02b..9b90401 100644
--- a/frontend/client/src/autotest/public/TestPlannerClient.html
+++ b/frontend/client/src/autotest/public/TestPlannerClient.html
@@ -36,6 +36,8 @@
       </div>
 
       <div id="overview" title="Overview">
+        <div id="overview_add_plan"></div>
+        <div id="overview_table"></div>
       </div>
 
       <div id="machine_view" title="Machine View">
diff --git a/frontend/client/src/autotest/public/testplannerclient.css b/frontend/client/src/autotest/public/testplannerclient.css
new file mode 100644
index 0000000..a6007aa
--- /dev/null
+++ b/frontend/client/src/autotest/public/testplannerclient.css
@@ -0,0 +1,12 @@
+.overview_table {
+  border-width: 2px;
+  border-style: solid;
+}
+
+.overview_table .header {
+  background-color: lightBlue;
+}
+
+.overview_table .even {
+  background-color: lightCyan;
+}
diff --git a/frontend/planner/planner_test_utils.py b/frontend/planner/planner_test_utils.py
index f4c9f53..1ce52d7 100644
--- a/frontend/planner/planner_test_utils.py
+++ b/frontend/planner/planner_test_utils.py
@@ -9,6 +9,7 @@
     _PLAN_NAME = 'plan'
     GOOD_STATUS_WORD = 'GOOD'
     RUNNING_STATUS_WORD = 'RUNNING'
+    FAIL_STATUS_WORD = 'FAIL'
 
     def _planner_common_setup(self):
         self._frontend_common_setup()
@@ -73,3 +74,5 @@
                 word=self.RUNNING_STATUS_WORD)
         self._good_status = tko_models.Status.objects.create(
                 word=self.GOOD_STATUS_WORD)
+        self._fail_status = tko_models.Status.objects.create(
+                word=self.FAIL_STATUS_WORD)
diff --git a/frontend/planner/rpc_interface.py b/frontend/planner/rpc_interface.py
index c6b0e23..51284b0 100644
--- a/frontend/planner/rpc_interface.py
+++ b/frontend/planner/rpc_interface.py
@@ -488,6 +488,66 @@
     return result
 
 
+def get_overview_data(plan_ids):
+    """
+    Gets the data for the Overview tab
+
+    @param plan_ids: A list of the plans, by id or name
+    @return A dictionary - keys are plan names, values are dictionaries of data:
+                machines: A list of dictionaries:
+                hostname: The machine's hostname
+                status: The host's status
+                passed: True if the machine passed the test plan. A 'pass' means
+                        that, for every test configuration in the plan, the
+                        machine had at least one AFE job with no failed tests.
+                        'passed' could also be None, meaning that this host is
+                        still running tests.
+                bugs: A list of the bugs filed
+                test_configs: A list of dictionaries, each representing a test
+                              config:
+                    complete: Number of hosts that have completed this test
+                              config
+                    estimated_runtime: Number of hours this test config is
+                                       expected to run on each host
+    """
+    plans = models.Plan.smart_get_bulk(plan_ids)
+    result = {}
+
+    for plan in plans:
+        machines = []
+        for host in plan.host_set.all():
+            machines.append({'hostname': host.host.hostname,
+                             'status': host.status(),
+                             'passed': rpc_utils.compute_passed(host)})
+
+        bugs = set()
+        for testrun in plan.testrun_set.all():
+            bugs.update(testrun.bugs.values_list('external_uid', flat=True))
+
+        test_configs = []
+        for test_config in plan.testconfig_set.all():
+            complete_statuses = afe_models.HostQueueEntry.COMPLETE_STATUSES
+            complete_jobs = test_config.job_set.filter(
+                    afe_job__hostqueueentry__status__in=complete_statuses)
+            complete_afe_jobs = afe_models.Job.objects.filter(
+                    id__in=complete_jobs.values_list('afe_job', flat=True))
+
+            complete_hosts = afe_models.Host.objects.filter(
+                    hostqueueentry__job__in=complete_afe_jobs)
+            complete_hosts |= test_config.skipped_hosts.all()
+
+            test_configs.append(
+                    {'complete': complete_hosts.distinct().count(),
+                     'estimated_runtime': test_config.estimated_runtime})
+
+        plan_data = {'machines': machines,
+                     'bugs': list(bugs),
+                     'test_configs': test_configs}
+        result[plan.name] = plan_data
+
+    return result
+
+
 def get_motd():
     return afe_rpc_utils.get_motd()
 
@@ -497,5 +557,6 @@
               'host_actions': sorted(failure_actions.HostAction.values),
               'test_actions': sorted(failure_actions.TestAction.values),
               'additional_parameter_types':
-                      sorted(model_attributes.AdditionalParameterType.values)}
+                      sorted(model_attributes.AdditionalParameterType.values),
+              'host_statuses': sorted(model_attributes.HostStatus.values)}
     return result
diff --git a/frontend/planner/rpc_interface_unittest.py b/frontend/planner/rpc_interface_unittest.py
index 0048307..f36d458 100644
--- a/frontend/planner/rpc_interface_unittest.py
+++ b/frontend/planner/rpc_interface_unittest.py
@@ -232,5 +232,52 @@
         self.assertEqual(sorted(actual), sorted(expected))
 
 
+    def _test_get_overview_data_helper(self, stage):
+        self._setup_active_plan()
+        self.god.stub_function(rpc_utils, 'compute_passed')
+        rpc_utils.compute_passed.expect_call(
+                self._plan.host_set.get(host=self.hosts[0])).and_return(None)
+        rpc_utils.compute_passed.expect_call(
+                self._plan.host_set.get(host=self.hosts[1])).and_return(None)
+
+        data = {'test_configs': [{'complete': 0, 'estimated_runtime': 1}],
+                'bugs': [],
+                'machines': [{'hostname': self.hosts[0].hostname,
+                              'status': 'Running',
+                              'passed': None},
+                             {'hostname': self.hosts[1].hostname,
+                              'status': 'Running',
+                              'passed': None}]}
+        if stage < 1:
+            return {self._plan.name: data}
+
+        tko_test = self._tko_job.test_set.create(kernel=self._tko_kernel,
+                                                 machine=self._tko_machine,
+                                                 status=self._fail_status)
+        test_run = self._plan.testrun_set.create(test_job=self._planner_job,
+                                                 tko_test=tko_test,
+                                                 host=self._planner_host)
+        self._afe_job.hostqueueentry_set.update(status='Completed')
+        self._planner_host.complete = True
+        self._planner_host.save()
+        test_run.bugs.create(external_uid='bug')
+        data['bugs'] = ['bug']
+        data['test_configs'][0]['complete'] = 1
+        data['machines'][0]['status'] = 'Finished'
+        return {self._plan.name: data}
+
+
+    def test_get_overview_data_no_progress(self):
+        self.assertEqual(self._test_get_overview_data_helper(0),
+                         rpc_interface.get_overview_data([self._plan.id]))
+        self.god.check_playback()
+
+
+    def test_get_overview_data_one_finished_with_bug(self):
+        self.assertEqual(self._test_get_overview_data_helper(1),
+                         rpc_interface.get_overview_data([self._plan.id]))
+        self.god.check_playback()
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/frontend/planner/rpc_utils.py b/frontend/planner/rpc_utils.py
index 9bf8a41..bcdca53 100644
--- a/frontend/planner/rpc_utils.py
+++ b/frontend/planner/rpc_utils.py
@@ -317,3 +317,26 @@
             skip_verify=(not run_verify),
             verify_params=verify_params,
             **additional_wrap_arguments)
+
+
+def compute_passed(host):
+    """
+    Returns True if the host can be considered to have passed its test plan
+
+    A 'pass' means that, for every test configuration in the plan, the machine
+    had at least one AFE job with no failed tests. 'passed' could also be None,
+    meaning that this host is still running tests.
+    """
+    if not host.complete:
+        return None
+
+    test_configs = host.plan.testconfig_set.exclude(skipped_hosts=host.host)
+    for test_config in test_configs:
+        for planner_job in test_config.job_set.all():
+            bad = planner_job.testrun_set.exclude(tko_test__status__word='GOOD')
+            if not bad:
+                break
+        else:
+            # Didn't break out of loop; this test config had no good jobs
+            return False
+    return True
diff --git a/frontend/planner/rpc_utils_unittest.py b/frontend/planner/rpc_utils_unittest.py
index d5579ec..a13b6d4 100644
--- a/frontend/planner/rpc_utils_unittest.py
+++ b/frontend/planner/rpc_utils_unittest.py
@@ -322,5 +322,41 @@
 
         self.assertEqual(actual, expected)
 
+
+    def test_compute_passed_incomplete(self):
+        self._setup_active_plan()
+        self._planner_host.complete = False
+        self._planner_host.save()
+        self.assertEqual(rpc_utils.compute_passed(self._planner_host), None)
+
+
+    def test_compute_passed_good(self):
+        self._setup_active_plan()
+        tko_test = self._tko_job.test_set.create(kernel=self._tko_kernel,
+                                                 status=self._good_status,
+                                                 machine=self._tko_machine)
+        self._plan.testrun_set.create(test_job=self._planner_job,
+                                      tko_test=tko_test,
+                                      host=self._planner_host)
+        self._planner_host.complete = True
+        self._planner_host.save()
+
+        self.assertEqual(rpc_utils.compute_passed(self._planner_host), True)
+
+
+    def test_compute_passed_bad(self):
+        self._setup_active_plan()
+        tko_test = self._tko_job.test_set.create(kernel=self._tko_kernel,
+                                                 status=self._fail_status,
+                                                 machine=self._tko_machine)
+        self._plan.testrun_set.create(test_job=self._planner_job,
+                                      tko_test=tko_test,
+                                      host=self._planner_host)
+        self._planner_host.complete = True
+        self._planner_host.save()
+
+        self.assertEqual(rpc_utils.compute_passed(self._planner_host), False)
+
+
 if __name__ == '__main__':
     unittest.main()