Initial checkin of new TKO interface.
git-svn-id: http://test.kernel.org/svn/autotest/trunk@1959 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/apache/conf/all-directives b/apache/conf/all-directives
index 792d4a4..b529718 100644
--- a/apache/conf/all-directives
+++ b/apache/conf/all-directives
@@ -1,3 +1,4 @@
Include "/usr/local/autotest/apache/conf/tko-directives"
+Include "/usr/local/autotest/apache/conf/new-tko-directives"
Include "/usr/local/autotest/apache/conf/django-directives"
Include "/usr/local/autotest/apache/conf/site-directives"
diff --git a/apache/conf/new-tko-directives b/apache/conf/new-tko-directives
new file mode 100644
index 0000000..70e12fa
--- /dev/null
+++ b/apache/conf/new-tko-directives
@@ -0,0 +1,13 @@
+Alias /new_tko "/usr/local/autotest/frontend/client/www/autotest.TkoClient"
+<Location "/new_tko">
+ DirectoryIndex TkoClient.html
+</Location>
+
+<Location "/new_tko/server">
+ SetHandler python-program
+ PythonHandler django.core.handlers.modpython
+ SetEnv DJANGO_SETTINGS_MODULE new_tko.settings
+ PythonDebug On
+ PythonPath "['/usr/local/autotest'] + sys.path"
+ PythonInterpreter tko_interpreter
+</Location>
diff --git a/frontend/client/TkoClient-compile b/frontend/client/TkoClient-compile
new file mode 100644
index 0000000..46e6074
--- /dev/null
+++ b/frontend/client/TkoClient-compile
@@ -0,0 +1,7 @@
+#!/bin/sh
+APPDIR=`dirname $0`;
+GWTDIR=`$APPDIR/gwt_dir`;
+java \
+ -cp "$APPDIR/src:$APPDIR/bin:$GWTDIR/gwt-user.jar:$GWTDIR/gwt-dev-linux.jar" \
+ -Djava.awt.headless=true \
+ com.google.gwt.dev.GWTCompiler -out "$APPDIR/www" "$@" autotest.TkoClient
diff --git a/frontend/client/TkoClient-shell b/frontend/client/TkoClient-shell
new file mode 100644
index 0000000..dd70842
--- /dev/null
+++ b/frontend/client/TkoClient-shell
@@ -0,0 +1,4 @@
+#!/bin/sh
+APPDIR=`dirname $0`;
+GWTDIR=`$APPDIR/gwt_dir`;
+java -cp "$APPDIR/src:$APPDIR/bin:$GWTDIR/gwt-user.jar:$GWTDIR/gwt-dev-linux.jar" com.google.gwt.dev.GWTShell -out "$APPDIR/www" "$@" http://localhost:8000/new_tko/server/autotest.TkoClient/TkoClient.html;
diff --git a/frontend/client/TkoClient.launch b/frontend/client/TkoClient.launch
new file mode 100644
index 0000000..15fd9c6
--- /dev/null
+++ b/frontend/client/TkoClient.launch
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
+<listEntry value="/AfeClient"/>
+</listAttribute>
+<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
+<listEntry value="4"/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
+<listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry containerPath="org.eclipse.jdt.launching.JRE_CONTAINER" javaProject="AfeClient" path="1" type="4"/> "/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry internalArchive="/AfeClient/src" path="3" type="2"/> "/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry id="org.eclipse.jdt.launching.classpathentry.defaultClasspath"> <memento project="AfeClient"/> </runtimeClasspathEntry> "/>
+<listEntry value="<?xml version="1.0" encoding="UTF-8"?> <runtimeClasspathEntry externalArchive="/usr/local/lib/gwt/gwt-dev-linux.jar" path="3" type="2"/> "/>
+</listAttribute>
+<booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
+<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.GWTShell"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-out www http://localhost:8000/new_tko/server/autotest.TkoClient/TkoClient.html"/>
+<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="AfeClient"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M"/>
+</launchConfiguration>
diff --git a/frontend/client/src/autotest/TkoClient.gwt.xml b/frontend/client/src/autotest/TkoClient.gwt.xml
new file mode 100644
index 0000000..44a2a9b
--- /dev/null
+++ b/frontend/client/src/autotest/TkoClient.gwt.xml
@@ -0,0 +1,13 @@
+<module>
+ <inherits name='com.google.gwt.user.User'/>
+ <inherits name='com.google.gwt.json.JSON'/>
+ <inherits name='com.google.gwt.http.HTTP'/>
+
+ <source path="tko"/>
+ <source path="common"/>
+ <entry-point class='autotest.tko.TkoClient'/>
+
+ <stylesheet src='standard.css'/>
+ <stylesheet src='afeclient.css'/>
+ <stylesheet src='tkoclient.css'/>
+</module>
diff --git a/frontend/client/src/autotest/afe/JobDetailView.java b/frontend/client/src/autotest/afe/JobDetailView.java
index 26026e9..fdd5952 100644
--- a/frontend/client/src/autotest/afe/JobDetailView.java
+++ b/frontend/client/src/autotest/afe/JobDetailView.java
@@ -68,7 +68,7 @@
@Override
protected void fetchData() {
- pointToResults(NO_URL, NO_URL);
+ pointToResults(NO_URL, NO_URL, NO_URL);
JSONObject params = new JSONObject();
params.put("id", new JSONNumber(jobId));
rpcProxy.rpcCall("get_jobs_summary", params, new JsonRpcCallback() {
@@ -103,7 +103,8 @@
abortButton.setVisible(!allFinishedCounts(counts));
String jobLogsId = jobId + "-" + owner;
- pointToResults(getResultsURL(jobId), getLogsURL(jobLogsId));
+ pointToResults(getResultsURL(jobId), getLogsURL(jobLogsId),
+ getOldResultsUrl(jobId));
String jobTitle = "Job: " + name + " (" + jobLogsId + ")";
displayObjectData(jobTitle);
@@ -213,7 +214,12 @@
});
}
- protected String getResultsURL(int jobId) {
+ private String getResultsURL(int jobId) {
+ return "/new_tko/#tab_id=spreadsheet_view&row=hostname&column=test_name&" +
+ "condition=job_tag+LIKE+'" + Integer.toString(jobId) + "-%2525'";
+ }
+
+ private String getOldResultsUrl(int jobId) {
return "/tko/compose_query.cgi?" +
"columns=test&rows=hostname&condition=tag%7E%27" +
Integer.toString(jobId) + "-%25%27&title=Report";
@@ -227,18 +233,17 @@
return Utils.getLogsURL(jobLogsId);
}
- protected void pointToResults(String resultsUrl, String logsUrl) {
- DOM.setElementProperty(DOM.getElementById("results_link"),
- "href", resultsUrl);
- DOM.setElementProperty(DOM.getElementById("raw_results_link"),
- "href", logsUrl);
+ protected void pointToResults(String resultsUrl, String logsUrl, String oldResultsUrl) {
+ DOM.getElementById("results_link").setAttribute("href", resultsUrl);
+ DOM.getElementById("old_results_link").setAttribute("href", oldResultsUrl);
+ DOM.getElementById("raw_results_link").setAttribute("href", logsUrl);
if (resultsUrl.equals(NO_URL)) {
tkoResultsHtml.setHTML("");
return;
}
RequestBuilder requestBuilder =
- new RequestBuilder(RequestBuilder.GET, resultsUrl + "&brief=1");
+ new RequestBuilder(RequestBuilder.GET, oldResultsUrl + "&brief=1");
try {
requestBuilder.sendRequest("", new RequestCallback() {
public void onError(Request request, Throwable exception) {
diff --git a/frontend/client/src/autotest/public/AfeClient.html b/frontend/client/src/autotest/public/AfeClient.html
index 1813b6f..d291838 100644
--- a/frontend/client/src/autotest/public/AfeClient.html
+++ b/frontend/client/src/autotest/public/AfeClient.html
@@ -7,21 +7,22 @@
<body>
<!-- gwt history support -->
- <iframe src="javascript:''" id="__gwt_historyFrame"
+ <iframe src="javascript:''" id="__gwt_historyFrame"
style="width:0;height:0;border:0"></iframe>
-
+
<span class="links-box" style="float: right;">
<b>Links</b><br>
<a href="server/admin">Admin interface</a><br>
<a href="/tko">Results database</a><br>
+ <a href="/new_tko">New results interface</a><br>
<a href="http://test.kernel.org/autotest">Documentation</a><br>
<a href="server/feeds/jobs/completed">Completed Jobs Feed</a><br>
<a href="server/feeds/jobs/failed">Failed Jobs Feed</a><br>
</span>
<img src="header.png" />
<br /><br />
-
+
<div id="tabs" class="hidden">
<div id="job_list" title="Job List">
<div id="job_control_links" class="job-control"></div>
@@ -32,7 +33,7 @@
<div id="job_pagination"></div>
<div id="job_table"></div>
</div>
-
+
<div id="view_job" title="View Job">
<span id="job_id_fetch" class="box-full">Fetch job by ID:
<span id="job_id_fetch_controls"></span>
@@ -52,25 +53,26 @@
<span id="view_timeout"></span> hours<br>
<span class="field-name">Status:</span>
<span id="view_status"></span><br>
-
+
<span class="field-name">
- <span id="view_control_type"></span> control file
+ <span id="view_control_type"></span> control file
(<span id="view_synch_type"></span>):
</span>
<pre><div id="view_control_file" class="box"></div></pre>
<span class="field-name">
Full results
- <a id="results_link" target="_blank">(open in new window)</a>
+ <a id="old_results_link" target="_blank">(open in new window)</a>
+ <a id="results_link" target="_blank">(new results interface)</a>
<a id="raw_results_link" target="_blank">(raw results logs)</a><br>
</span>
-
+
<span id="tko_results"></span><br>
-
+
<span class="field-name">Hosts</span>
<div id="job_hosts_table"></div>
</div>
</div>
-
+
<div id="create_job" title="Create Job">
<table class="form-table">
<tr><td class="field-name">Job name:</td>
@@ -124,11 +126,11 @@
</tr>
</table>
</div>
-
+
<div id="hosts" title="Host List">
<div id="hosts_list"></div>
</div>
-
+
<div id="view_host" title="View Host">
<span id="view_host_fetch" class="box-full">Fetch host:
<span id="view_host_fetch_controls"></span>
@@ -153,7 +155,7 @@
</div>
<br>
<div id="error_log"></div>
-
+
<!-- for debugging only -->
<div id="error_display"></div>
</body>
diff --git a/frontend/client/src/autotest/public/TkoClient.html b/frontend/client/src/autotest/public/TkoClient.html
new file mode 100644
index 0000000..acaf655
--- /dev/null
+++ b/frontend/client/src/autotest/public/TkoClient.html
@@ -0,0 +1,106 @@
+<html>
+ <head>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <title>Autotest Frontend</title>
+ <script type="text/javascript" language='javascript'
+ src='autotest.TkoClient.nocache.js'>
+ </script>
+ </head>
+
+ <body>
+ <!-- gwt history support -->
+ <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1'
+ style="position:absolute;width:0;height:0;border:0"></iframe>
+
+
+ <span class="links-box" style="float: right;">
+ <b>Links</b><br>
+ <a href="/afe">Autotest Frontend</a><br>
+ <a href="server/admin">Admin interface</a><br>
+ <a href="http://test.kernel.org/autotest">Documentation</a><br>
+ </span>
+ <img src="header.png" />
+ <br /><br />
+
+ <div id="tabs" class="hidden">
+ <div id="common_panel">
+ <!-- Not a tab, but the common panel shown for all tabs -->
+ <div id="common_condition_div">
+ <span class="field-name">SQL filtering condition:</span>
+ <a href="http://test.kernel.org/autotest/TkoHowTo#filtering" target="_blank">Help</a>
+ <span id="common_quick_reference"></span><br>
+ <span id="common_sql_input"></span><br>
+ </div>
+ </div>
+
+ <div id="spreadsheet_view" title="Spreadsheet">
+ <table><tr valign="top">
+ <td class="field-name">Rows:</td>
+ <td id="ss_row_select"></td>
+ <td class="field-name">Columns:</td>
+ <td id="ss_column_select"></td>
+ <td id="ss_swap"></td>
+ </tr></table>
+ <span id="ss_query_controls"></span><br>
+
+ <span id="ss_job_completion"></span>
+
+ <!-- table is necessary here to get layout right -->
+ <table><tr>
+ <td id="ss_actions"></td>
+ </tr></table>
+ <div id="ss_spreadsheet"></div>
+ </div>
+
+ <div id="table_view" title="Table">
+ <span class="field-name">Columns:</span><br>
+ <span id="table_column_select"></span><br>
+ <span id="table_actions"></span>
+ <span id="table_query_controls"></span>
+
+ <div id="table_table"></div>
+ </div>
+
+ <div id="graphing_view" title="Graphing">
+ <span id="graphing_frontend"></span>
+ </div>
+
+ <div id="test_detail_view" title="Test details">
+ <span id="td_fetch" class="box-full">Fetch test by ID:
+ <span id="td_fetch_controls"></span>
+ </span><br><br>
+ <div id="td_title" class="title"></div><br>
+ <div id="td_data">
+ <span class="field-name">Test:</span>
+ <span id="td_test"></span><br>
+ <span class="field-name">Job tag:</span>
+ <span id="td_job_tag"></span><br>
+ <span class="field-name">Job name:</span>
+ <span id="td_job_name"></span><br>
+ <span class="field-name">Status:</span>
+ <span id="td_status"></span><br>
+ <span class="field-name">Reason:</span>
+ <span id="td_reason"></span><br>
+ <span class="field-name">Test started:</span>
+ <span id="td_test_started"></span><br>
+ <span class="field-name">Test finished:</span>
+ <span id="td_test_finished"></span><br>
+ <span class="field-name">Host:</span>
+ <span id="td_hostname"></span><br>
+ <span class="field-name">Platform:</span>
+ <span id="td_platform"></span><br>
+ <span class="field-name">Kernel:</span>
+ <span id="td_kernel"></span><br>
+ <br>
+
+ <span class="field-name">Key log files:</span>
+ <a id="td_view_logs_link" target="_blank">(view all logs)</a>
+ <div id="td_log_files"></div>
+ </div>
+ </div>
+
+ </div>
+ <br>
+ <div id="error_log"></div>
+ </body>
+</html>
diff --git a/frontend/client/src/autotest/public/tkoclient.css b/frontend/client/src/autotest/public/tkoclient.css
new file mode 100644
index 0000000..989443a
--- /dev/null
+++ b/frontend/client/src/autotest/public/tkoclient.css
@@ -0,0 +1,53 @@
+/* this combination of styles makes a div clip it's contents */
+div.clipper {
+ position: relative;
+ overflow: hidden;
+}
+
+/* necessary to get row headers aligned with table */
+table.spreadsheet-parent td {
+ vertical-align: top;
+}
+
+/* allow headers to scroll with table */
+table.spreadsheet-headers {
+ position: relative;
+}
+
+.spreadsheet-headers td, .spreadsheet-data td {
+ border-bottom: 1px solid gray;
+ border-right: 1px solid gray;
+}
+
+.spreadsheet-headers td {
+ background: #CCDDFF;
+}
+
+/* don't wrap cells automatically */
+.spreadsheet-data td {
+ white-space: pre;
+}
+
+.spreadsheet-data div, .spreadsheet-headers div {
+ padding: 2px;
+ cursor: pointer;
+}
+
+/* allow empty cells to override the clickable cursor */
+div.spreadsheet-cell-nonclickable {
+ cursor: default;
+}
+
+.highlighted {
+ background-color: #FFFFBB;
+}
+
+.log-file-panel {
+ width: 100%;
+ margin-bottom: 2px;
+}
+
+.log-file-text {
+ white-space: pre;
+ font-family: monospace;
+}
diff --git a/frontend/client/src/autotest/tko/CommonPanel.java b/frontend/client/src/autotest/tko/CommonPanel.java
new file mode 100644
index 0000000..f25c8b0
--- /dev/null
+++ b/frontend/client/src/autotest/tko/CommonPanel.java
@@ -0,0 +1,142 @@
+package autotest.tko;
+
+import autotest.common.Utils;
+import autotest.common.ui.ElementWidget;
+import autotest.common.ui.SimpleHyperlink;
+import autotest.tko.TkoUtils.FieldInfo;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.TextArea;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+
+import java.util.Map;
+
+class CommonPanel extends Composite implements ClickListener, PositionCallback {
+ private static final String SHOW_QUICK_REFERENCE = "Show quick reference";
+ private static final String HIDE_QUICK_REFERENCE = "Hide quick reference";
+ private static CommonPanel theInstance = new CommonPanel();
+
+ private TextArea customSqlBox = new TextArea();
+ private SimpleHyperlink quickReferenceLink = new SimpleHyperlink(SHOW_QUICK_REFERENCE);
+ private PopupPanel quickReferencePopup;
+ private String currentCondition = "";
+
+ private CommonPanel() {
+ ElementWidget panelElement = new ElementWidget(DOM.getElementById("common_panel"));
+ panelElement.removeFromDocument();
+ initWidget(panelElement);
+ }
+
+ public void initialize() {
+ customSqlBox.setSize("50em", "5em");
+ quickReferenceLink.addClickListener(this);
+ RootPanel.get("common_sql_input").add(customSqlBox);
+ RootPanel.get("common_quick_reference").add(quickReferenceLink);
+
+ generateQuickReferencePopup();
+ }
+
+ public static CommonPanel getPanel() {
+ return theInstance;
+ }
+
+ /**
+ * For testability.
+ */
+ public static void setInstance(CommonPanel panel) {
+ theInstance = panel;
+ }
+
+ public void setConditionVisible(boolean visible) {
+ RootPanel.get("common_condition_div").setVisible(visible);
+ }
+
+ public String getSqlCondition() {
+ return customSqlBox.getText().trim();
+ }
+
+ public void setSqlCondition(String text) {
+ customSqlBox.setText(text);
+ saveSqlCondition();
+ }
+
+ public void saveSqlCondition() {
+ currentCondition = getSqlCondition();
+ }
+
+ public String getSavedCondition() {
+ return currentCondition;
+ }
+
+ public String getRefinedCondition(TestSet tests) {
+ String newCondition = tests.getCondition();
+ if (newCondition.equals("") || newCondition.equals(currentCondition)) {
+ return currentCondition;
+ }
+ return appendCondition(currentCondition, "(" + newCondition + ")");
+ }
+
+ public void refineCondition(TestSet tests) {
+ setSqlCondition(getRefinedCondition(tests));
+ }
+
+ private static String appendCondition(String condition, String toAppend) {
+ if (!condition.equals(""))
+ condition += " AND ";
+ return condition + toAppend;
+ }
+
+ public void handleHistoryArguments(Map<String, String> arguments) {
+ setSqlCondition(arguments.get("condition"));
+ }
+
+ public void addHistoryArguments(Map<String, String> arguments) {
+ arguments.put("condition", getSavedCondition());
+ }
+
+ public void fillDefaultHistoryValues(Map<String, String> arguments) {
+ Utils.setDefaultValue(arguments, "condition", "");
+ }
+
+ public void onClick(Widget sender) {
+ assert sender == quickReferenceLink;
+ if (isQuickReferenceShowing()) {
+ quickReferencePopup.hide();
+ quickReferenceLink.setText(SHOW_QUICK_REFERENCE);
+ } else {
+ quickReferencePopup.setPopupPositionAndShow(this);
+ quickReferenceLink.setText(HIDE_QUICK_REFERENCE);
+ }
+ }
+
+ private boolean isQuickReferenceShowing() {
+ return quickReferenceLink.getText().equals(HIDE_QUICK_REFERENCE);
+ }
+
+ private void generateQuickReferencePopup() {
+ FlexTable fieldTable = new FlexTable();
+ fieldTable.setText(0, 0, "Name");
+ fieldTable.setText(0, 1, "Field");
+ fieldTable.getRowFormatter().setStyleName(0, "data-row-header");
+ int row = 1;
+ for (FieldInfo fieldInfo : TkoUtils.getFieldList("all_fields")) {
+ fieldTable.setText(row, 0, fieldInfo.name);
+ fieldTable.setText(row, 1, fieldInfo.field);
+ row++;
+ }
+ quickReferencePopup = new PopupPanel(false);
+ quickReferencePopup.add(fieldTable);
+ }
+
+ public void setPosition(int offsetWidth, int offsetHeight) {
+ quickReferencePopup.setPopupPosition(
+ customSqlBox.getAbsoluteLeft() + customSqlBox.getOffsetWidth(),
+ customSqlBox.getAbsoluteTop());
+ }
+}
diff --git a/frontend/client/src/autotest/tko/CompositeTestSet.java b/frontend/client/src/autotest/tko/CompositeTestSet.java
new file mode 100644
index 0000000..60abf65
--- /dev/null
+++ b/frontend/client/src/autotest/tko/CompositeTestSet.java
@@ -0,0 +1,26 @@
+package autotest.tko;
+
+import autotest.common.Utils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class CompositeTestSet implements TestSet {
+ private List<TestSet> testSets = new ArrayList<TestSet>();
+
+ public void add(TestSet tests) {
+ testSets.add(tests);
+ }
+
+ public String getCondition() {
+ List<String> conditionParts = new ArrayList<String>();
+ for(TestSet testSet : testSets) {
+ conditionParts.add("(" + testSet.getCondition() + ")");
+ }
+ return Utils.joinStrings(" OR ", conditionParts);
+ }
+
+ public boolean isSingleTest() {
+ return testSets.size() == 1 && testSets.get(0).isSingleTest();
+ }
+}
diff --git a/frontend/client/src/autotest/tko/ConditionTabView.java b/frontend/client/src/autotest/tko/ConditionTabView.java
new file mode 100644
index 0000000..823be1b
--- /dev/null
+++ b/frontend/client/src/autotest/tko/ConditionTabView.java
@@ -0,0 +1,52 @@
+package autotest.tko;
+
+import autotest.common.ui.TabView;
+
+import java.util.Map;
+
+abstract class ConditionTabView extends TabView {
+ protected static CommonPanel commonPanel = CommonPanel.getPanel();
+ private String lastCondition = "";
+ private boolean needsRefresh;
+
+ protected void checkForHistoryChanges(Map<String, String> newParameters) {
+ if (!getHistoryArguments().equals(newParameters)) {
+ needsRefresh = true;
+ }
+ }
+
+ protected void setNeedsRefresh(boolean needsRefresh) {
+ this.needsRefresh = needsRefresh;
+ }
+
+ protected abstract boolean hasFirstQueryOccurred();
+
+ @Override
+ public void display() {
+ ensureInitialized();
+ commonPanel.setConditionVisible(true);
+ if (needsRefresh || !commonPanel.getSavedCondition().equals(lastCondition)) {
+ saveCondition();
+ refresh();
+ needsRefresh = false;
+ }
+ }
+
+ protected void saveCondition() {
+ commonPanel.saveSqlCondition();
+ lastCondition = commonPanel.getSavedCondition();
+ }
+
+ @Override
+ public void handleHistoryArguments(Map<String, String> arguments) {
+ if (!hasFirstQueryOccurred()) {
+ needsRefresh = true;
+ }
+ commonPanel.fillDefaultHistoryValues(arguments);
+ fillDefaultHistoryValues(arguments);
+ checkForHistoryChanges(arguments);
+ commonPanel.handleHistoryArguments(arguments);
+ }
+
+ protected abstract void fillDefaultHistoryValues(Map<String, String> arguments);
+}
diff --git a/frontend/client/src/autotest/tko/ConditionTestSet.java b/frontend/client/src/autotest/tko/ConditionTestSet.java
new file mode 100644
index 0000000..0f85988
--- /dev/null
+++ b/frontend/client/src/autotest/tko/ConditionTestSet.java
@@ -0,0 +1,54 @@
+package autotest.tko;
+
+import autotest.common.Utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class ConditionTestSet implements TestSet {
+ private Map<String,String> fields = new HashMap<String,String>();
+ private boolean isSingleTest;
+ private String initialCondition = "";
+
+ public ConditionTestSet(boolean isSingleTest) {
+ this.isSingleTest = isSingleTest;
+ }
+
+ public ConditionTestSet(boolean isSingleTest, String initialCondition) {
+ this(isSingleTest);
+ this.initialCondition = initialCondition;
+ }
+
+ public void setField(String field, String value) {
+ fields.put(field, value);
+ }
+
+ public void setFields(List<String> fields, List<String> values) {
+ assert fields.size() == values.size();
+ for (int i = 0; i < fields.size(); i++) {
+ setField(fields.get(i), values.get(i));
+ }
+ }
+
+ public String getCondition() {
+ ArrayList<String> parts = new ArrayList<String>();
+ for (Map.Entry<String, String> entry : fields.entrySet()) {
+ parts.add(entry.getKey() + " = '" + escapeQuotes(entry.getValue()) + "'");
+ }
+ if (!initialCondition.trim().equals("")) {
+ parts.add(initialCondition);
+ }
+ return Utils.joinStrings(" AND ", parts);
+ }
+
+ private String escapeQuotes(String value) {
+ // TODO: make this more sophisticated if it becomes necessary
+ return value.replace("'", "\\'");
+ }
+
+ public boolean isSingleTest() {
+ return isSingleTest;
+ }
+}
diff --git a/frontend/client/src/autotest/tko/ExistingGraphsFrontend.java b/frontend/client/src/autotest/tko/ExistingGraphsFrontend.java
new file mode 100644
index 0000000..1ad4575
--- /dev/null
+++ b/frontend/client/src/autotest/tko/ExistingGraphsFrontend.java
@@ -0,0 +1,205 @@
+package autotest.tko;
+
+import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.StaticDataRepository;
+import autotest.common.Utils;
+
+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.Window;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.FocusListener;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.MultiWordSuggestOracle;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestionEvent;
+import com.google.gwt.user.client.ui.SuggestionHandler;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.HashMap;
+import java.util.HashSet;
+
+public class ExistingGraphsFrontend extends Composite {
+
+ private JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+ private CheckBox normalize = new CheckBox("Normalize Performance (allows multiple benchmarks" +
+ " on one graph)");
+ private MultiWordSuggestOracle oracle = new MultiWordSuggestOracle();
+ private TextBox hostname = new TextBox();
+ private SuggestBox hostnameSuggest = new SuggestBox(oracle, hostname);
+ private ListBox benchmark = new ListBox();
+ private TextBox kernel = new TextBox();
+ private JSONObject hostsAndTests = null;
+ private Button graphButton = new Button("Graph");
+ FlexTable table = new FlexTable();
+
+ public ExistingGraphsFrontend() {
+ normalize.addClickListener(new ClickListener() {
+ public void onClick(Widget w) {
+ benchmark.setMultipleSelect(normalize.isChecked());
+ int selectedIndex = benchmark.getSelectedIndex();
+ for (int i = 0; i < benchmark.getItemCount(); i++) {
+ benchmark.setItemSelected(i, i == selectedIndex);
+ }
+ }
+ });
+
+ hostnameSuggest.addFocusListener(new FocusListener() {
+ public void onLostFocus(Widget w) {
+ refreshTests();
+ }
+
+ public void onFocus(Widget w) {
+ // Don't do anything
+ }
+ });
+ hostnameSuggest.addEventHandler(new SuggestionHandler() {
+ public void onSuggestionSelected(SuggestionEvent s) {
+ refreshTests();
+ }
+ });
+
+ graphButton.addClickListener(new ClickListener() {
+ public void onClick(Widget w) {
+ showGraph();
+ }
+ });
+
+ kernel.setText("all");
+
+ table.setWidget(0, 0, normalize);
+ table.getFlexCellFormatter().setColSpan(0, 0, 2);
+ addControl("Hostname:", hostnameSuggest);
+ addControl("Benchmark:", benchmark);
+ addControl("Kernel:", kernel);
+ table.setWidget(table.getRowCount(), 1, graphButton);
+
+ table.getColumnFormatter().setWidth(0, "1px");
+
+ initWidget(table);
+ }
+
+ public void refresh() {
+ setEnabled(false);
+ rpcProxy.rpcCall("get_hosts_and_tests", new JSONObject(), new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ hostsAndTests = result.isObject();
+ oracle.clear();
+ for (String host : hostsAndTests.keySet()) {
+ oracle.add(host);
+ }
+ setEnabled(true);
+ }
+ });
+ }
+
+ private void addControl(String text, Widget widget) {
+ int row = table.getRowCount();
+ table.setText(row, 0, text);
+ table.setWidget(row, 1, widget);
+ table.getFlexCellFormatter().setStylePrimaryName(row, 0, "field-name");
+ table.getFlexCellFormatter().setVerticalAlignment(row, 0, HasVerticalAlignment.ALIGN_TOP);
+ }
+
+ private void setEnabled(boolean enabled) {
+ normalize.setEnabled(enabled);
+ hostname.setEnabled(enabled);
+ benchmark.setEnabled(enabled);
+ kernel.setEnabled(enabled);
+ graphButton.setEnabled(enabled);
+ }
+
+ private void refreshTests() {
+ JSONValue value = hostsAndTests.get(hostnameSuggest.getText());
+ if (value == null) {
+ return;
+ }
+
+ HashSet<String> selectedTests = new HashSet<String>();
+ for (int i = 0; i < benchmark.getItemCount(); i++) {
+ if (benchmark.isItemSelected(i)) {
+ selectedTests.add(benchmark.getValue(i));
+ }
+ }
+
+ JSONArray tests = value.isObject().get("tests").isArray();
+ benchmark.clear();
+ for (int i = 0; i < tests.size(); i++) {
+ String test = tests.get(i).isString().stringValue();
+ benchmark.addItem(test);
+ if (selectedTests.contains(test)) {
+ benchmark.setItemSelected(i, true);
+ }
+ }
+ }
+
+ private void showGraph() {
+ String hostnameStr = hostnameSuggest.getText();
+
+ JSONValue value = hostsAndTests.get(hostnameStr);
+ if (value == null) {
+ return;
+ }
+
+ String url;
+ HashMap<String, String> args = new HashMap<String, String>();
+ args.put("kernel", kernel.getText());
+
+ if (normalize.isChecked()) {
+ url = "/tko/machine_aggr.cgi?";
+ final JSONArray tests = new JSONArray();
+ for (int i = 0; i < benchmark.getItemCount(); i++) {
+ if (benchmark.isItemSelected(i)) {
+ tests.set(tests.size(), new JSONString(benchmark.getValue(i)));
+ }
+ }
+
+ args.put("machine", hostnameStr);
+
+ StringBuilder arg = new StringBuilder();
+ for (int i = 0; i < tests.size(); i++) {
+ String test = tests.get(i).isString().stringValue();
+ String key = getKey(test);
+ if (i != 0) {
+ arg.append(",");
+ }
+ arg.append(test);
+ arg.append(":");
+ arg.append(key);
+ }
+ args.put("benchmark_key", arg.toString());
+ } else {
+ int benchmarkIndex = benchmark.getSelectedIndex();
+ if (benchmarkIndex == -1) {
+ return;
+ }
+
+ url = "/tko/machine_test_attribute_graph.cgi?";
+
+ JSONObject hostObject = value.isObject();
+ int machine = (int) hostObject.get("id").isNumber().doubleValue();
+ String benchmarkStr = benchmark.getValue(benchmarkIndex);
+
+ args.put("machine", String.valueOf(machine));
+ args.put("benchmark", benchmarkStr);
+ args.put("key", getKey(benchmarkStr));
+ }
+ Window.open(url + Utils.encodeUrlArguments(args), "_blank", "");
+ }
+
+ private String getKey(String benchmark) {
+ JSONObject benchmarkKey =
+ StaticDataRepository.getRepository().getData("benchmark_key").isObject();
+ return benchmarkKey.get(benchmark.replaceAll("\\..*", "")).isString().stringValue();
+ }
+}
diff --git a/frontend/client/src/autotest/tko/FragmentedTable.java b/frontend/client/src/autotest/tko/FragmentedTable.java
new file mode 100644
index 0000000..a20e939
--- /dev/null
+++ b/frontend/client/src/autotest/tko/FragmentedTable.java
@@ -0,0 +1,137 @@
+package autotest.tko;
+
+import autotest.common.ui.RightClickTable;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.HTMLTable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Customized table class supporting multiple tbody elements. It is modified to support input
+ * handling, getRowCount(), getCellCount(), and getCellFormatter().getElement(). getElement()
+ * also works. Calls to other methods aren't guaranteed to work.
+ */
+class FragmentedTable extends RightClickTable {
+ public class FragmentedCellFormatter extends HTMLTable.CellFormatter {
+ @Override
+ public Element getElement(int row, int column) {
+ checkCellBounds(row, column);
+ Element bodyElem = bodyElems.get(getFragmentIndex(row));
+ return getCellElement(bodyElem, getRowWithinFragment(row), column);
+ }
+
+ /**
+ * Native method to efficiently get a td element from a tbody. Copied from GWT's
+ * HTMLTable.java.
+ */
+ private native Element getCellElement(Element tbody, int row, int col) /*-{
+ return tbody.rows[row].cells[col];
+ }-*/;
+ }
+
+ private List<Element> bodyElems = new ArrayList<Element>();
+ private int totalRowCount;
+ private int rowsPerFragment;
+
+ public FragmentedTable() {
+ super();
+ setCellFormatter(new FragmentedCellFormatter());
+ }
+
+ /**
+ * This method must be called after added or removing tbody elements and before using other
+ * functionality (accessing cell elements, input handling, etc.).
+ */
+ public void updateBodyElems() {
+ totalRowCount = 0;
+ Element tbody = DOM.getFirstChild(getElement());
+ for(; tbody != null; tbody = DOM.getNextSibling(tbody)) {
+ assert tbody.getTagName().equalsIgnoreCase("tbody");
+ bodyElems.add(tbody);
+ totalRowCount += getRowCount(tbody);
+ }
+ }
+
+ public void reset() {
+ bodyElems.clear();
+ TkoUtils.clearDomChildren(getElement());
+ }
+
+ private int getRowWithinFragment(int row) {
+ return row % rowsPerFragment;
+ }
+
+ private int getFragmentIndex(int row) {
+ return row / rowsPerFragment;
+ }
+
+ @Override
+ protected RowColumn getCellPosition(Element td) {
+ Element tr = DOM.getParent(td);
+ Element body = DOM.getParent(tr);
+ int fragmentIndex = DOM.getChildIndex(getElement(), body);
+ int rowWithinFragment = DOM.getChildIndex(body, tr);
+ int row = fragmentIndex * rowsPerFragment + rowWithinFragment;
+ int column = DOM.getChildIndex(tr, td);
+ return new RowColumn(row, column);
+ }
+
+ /**
+ * This is a modified version of getEventTargetCell() from HTMLTable.java.
+ */
+ @Override
+ protected Element getEventTargetCell(Event event) {
+ Element td = DOM.eventGetTarget(event);
+ for (; td != null; td = DOM.getParent(td)) {
+ // If it's a TD, it might be the one we're looking for.
+ if (DOM.getElementProperty(td, "tagName").equalsIgnoreCase("td")) {
+ // Make sure it's directly a part of this table before returning
+ // it.
+ Element tr = DOM.getParent(td);
+ Element body = DOM.getParent(tr);
+ Element tableElem = DOM.getParent(body);
+ if (tableElem == getElement()) {
+ return td;
+ }
+ }
+ // If we run into this table's element, we're out of options.
+ if (td == getElement()) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public int getCellCount(int row) {
+ Element bodyElem = bodyElems.get(getFragmentIndex(row));
+ return getCellCount(bodyElem, getRowWithinFragment(row));
+ }
+
+ @Override
+ public int getRowCount() {
+ return totalRowCount;
+ }
+
+ private native int getRowCount(Element tbody) /*-{
+ return tbody.rows.length;
+ }-*/;
+
+ private native int getCellCount(Element tbody, int row) /*-{
+ return tbody.rows[row].cells.length;
+ }-*/;
+
+ /**
+ * This must be called before using other functionality (accessing cell elements, input
+ * handling, etc.).
+ * @param rowsPerFragment The number of rows in each tbody. The last tbody may have fewer
+ * rows. All others must have exactly this number of rows.
+ */
+ public void setRowsPerFragment(int rowsPerFragment) {
+ this.rowsPerFragment = rowsPerFragment;
+ }
+}
diff --git a/frontend/client/src/autotest/tko/GraphingView.java b/frontend/client/src/autotest/tko/GraphingView.java
new file mode 100644
index 0000000..a7b2c6d
--- /dev/null
+++ b/frontend/client/src/autotest/tko/GraphingView.java
@@ -0,0 +1,32 @@
+package autotest.tko;
+
+import autotest.common.ui.TabView;
+
+import com.google.gwt.user.client.ui.RootPanel;
+
+public class GraphingView extends TabView {
+
+ private ExistingGraphsFrontend existingGraphsFrontend = new ExistingGraphsFrontend();
+
+ @Override
+ public void initialize() {
+ RootPanel.get("graphing_frontend").add(existingGraphsFrontend);
+ }
+
+ @Override
+ public String getElementId() {
+ return "graphing_view";
+ }
+
+ @Override
+ public void refresh() {
+ super.refresh();
+ existingGraphsFrontend.refresh();
+ }
+
+ @Override
+ public void display() {
+ super.display();
+ CommonPanel.getPanel().setConditionVisible(false);
+ }
+}
diff --git a/frontend/client/src/autotest/tko/HeaderSelect.java b/frontend/client/src/autotest/tko/HeaderSelect.java
new file mode 100644
index 0000000..b018a1e
--- /dev/null
+++ b/frontend/client/src/autotest/tko/HeaderSelect.java
@@ -0,0 +1,103 @@
+package autotest.tko;
+
+import autotest.common.ui.DoubleListSelector;
+import autotest.common.ui.SimpleHyperlink;
+import autotest.common.ui.DoubleListSelector.Item;
+
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.StackPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.List;
+
+class HeaderSelect extends Composite implements ClickListener {
+ private final static String SWITCH_TO_MULTIPLE = "Switch to multiple";
+ private final static String SWITCH_TO_SINGLE = "Switch to single";
+
+ private ListBox listBox = new ListBox();
+ private DoubleListSelector doubleList = new DoubleListSelector();
+ private StackPanel stack = new StackPanel();
+ private SimpleHyperlink switchLink = new SimpleHyperlink(SWITCH_TO_MULTIPLE);
+
+ public HeaderSelect() {
+ stack.add(listBox);
+ stack.add(doubleList);
+
+ Panel panel = new VerticalPanel();
+ panel.add(stack);
+ panel.add(switchLink);
+ initWidget(panel);
+
+ switchLink.addClickListener(this);
+ }
+
+ public void addItem(String name, String value) {
+ listBox.addItem(name, value);
+ doubleList.addItem(name, value);
+ }
+
+ public List<Item> getSelectedItems() {
+ if (!isDoubleSelectActive()) {
+ copyListSelectionToDoubleList();
+ }
+ return doubleList.getSelectedItems();
+ }
+
+ private boolean isDoubleSelectActive() {
+ return switchLink.getText().equals(SWITCH_TO_SINGLE);
+ }
+
+ public void selectItemsByValue(List<String> values) {
+ if (values.size() > 1 && !isDoubleSelectActive()) {
+ onClick(switchLink);
+ }
+
+ if (isDoubleSelectActive()) {
+ doubleList.deselectAll();
+ for (String value : values) {
+ doubleList.selectItemByValue(value);
+ }
+ } else {
+ setListBoxSelection(values.get(0));
+ }
+ }
+
+ public void onClick(Widget sender) {
+ assert sender == switchLink;
+ if (isDoubleSelectActive()) {
+ if (doubleList.getSelectedItemCount() > 0) {
+ setListBoxSelection(doubleList.getSelectedItems().get(0).value);
+ }
+ stack.showStack(0);
+ switchLink.setText(SWITCH_TO_MULTIPLE);
+ } else {
+ copyListSelectionToDoubleList();
+ stack.showStack(1);
+ switchLink.setText(SWITCH_TO_SINGLE);
+ }
+ }
+
+ private void copyListSelectionToDoubleList() {
+ doubleList.deselectAll();
+ doubleList.selectItemByValue(getListBoxSelection());
+ }
+
+ private void setListBoxSelection(String value) {
+ for (int i = 0; i < listBox.getItemCount(); i++) {
+ if (listBox.getValue(i).equals(value)) {
+ listBox.setSelectedIndex(i);
+ return;
+ }
+ }
+
+ throw new IllegalArgumentException("No item with value " + value);
+ }
+
+ private String getListBoxSelection() {
+ return listBox.getValue(listBox.getSelectedIndex());
+ }
+}
diff --git a/frontend/client/src/autotest/tko/SavedQueriesControl.java b/frontend/client/src/autotest/tko/SavedQueriesControl.java
new file mode 100644
index 0000000..6309cfe
--- /dev/null
+++ b/frontend/client/src/autotest/tko/SavedQueriesControl.java
@@ -0,0 +1,201 @@
+package autotest.tko;
+
+import autotest.common.CustomHistory;
+import autotest.common.JSONArrayList;
+import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.StaticDataRepository;
+import autotest.common.CustomHistory.CustomHistoryListener;
+import autotest.common.ui.NotifyManager;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONNumber;
+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.History;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DialogBox;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.Map;
+
+class SavedQueriesControl extends Composite
+ implements ChangeListener, ClickListener, CustomHistoryListener {
+ public static final String HISTORY_TOKEN = "saved_query";
+
+ private static final String ADD_QUERY = "Save current...";
+ private static final String DELETE_QUERY = "Delete query...";
+ private static final String DEFAULT_ITEM = "Saved queries...";
+
+ private static JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+ private static NotifyManager notifyManager = NotifyManager.getInstance();
+
+ private ListBox queryList = new ListBox();
+ private QueryActionDialog<TextBox> addQueryDialog;
+ private QueryActionDialog<ListBox> deleteQueryDialog;
+
+ private static class QueryActionDialog<T extends Widget> extends DialogBox {
+ public T widget;
+ public Button actionButton, cancelButton;
+
+ public QueryActionDialog(T widget, String widgetString, String actionString) {
+ super(false, true);
+ this.widget = widget;
+ Panel dialogPanel = new VerticalPanel();
+ Panel widgetPanel = new HorizontalPanel();
+ widgetPanel.add(new Label(widgetString));
+ widgetPanel.add(widget);
+ dialogPanel.add(widgetPanel);
+
+ Panel buttonPanel = new HorizontalPanel();
+ actionButton = new Button(actionString);
+ cancelButton = new Button("Cancel");
+ buttonPanel.add(actionButton);
+ buttonPanel.add(cancelButton);
+ dialogPanel.add(buttonPanel);
+ add(dialogPanel);
+
+ cancelButton.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ hide();
+ }
+ });
+ }
+ }
+
+ public SavedQueriesControl() {
+ queryList.addChangeListener(this);
+ populateMainList();
+ initWidget(queryList);
+
+ addQueryDialog = new QueryActionDialog<TextBox>(new TextBox(),
+ "Enter query name:", "Save query");
+ addQueryDialog.actionButton.addClickListener(this);
+ deleteQueryDialog = new QueryActionDialog<ListBox>(new ListBox(),
+ "Select query:", "Delete query");
+ deleteQueryDialog.actionButton.addClickListener(this);
+
+ CustomHistory.addHistoryListener(this);
+ }
+
+ private void populateMainList() {
+ queryList.clear();
+ queryList.addItem(DEFAULT_ITEM);
+ queryList.addItem(ADD_QUERY);
+ queryList.addItem(DELETE_QUERY);
+ fillQueryList(queryList);
+ }
+
+ private void fillQueryList(final ListBox list) {
+ StaticDataRepository staticData = StaticDataRepository.getRepository();
+ JSONObject args = new JSONObject();
+ args.put("owner", staticData.getData("user_login"));
+ rpcProxy.rpcCall("get_saved_queries", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ for (JSONObject query : new JSONArrayList<JSONObject>(result.isArray())) {
+ int id = (int) query.get("id").isNumber().doubleValue();
+ list.addItem(query.get("name").isString().stringValue(),
+ Integer.toString(id));
+ }
+ }
+ });
+ }
+
+ public void onChange(Widget sender) {
+ int selected = queryList.getSelectedIndex();
+ queryList.setSelectedIndex(0); // set it back to the default
+
+ String queryName = queryList.getItemText(selected);
+ if (queryName.equals(DEFAULT_ITEM)) {
+ return;
+ }
+ if (queryName.equals(ADD_QUERY)) {
+ addQueryDialog.widget.setText("");
+ addQueryDialog.center();
+ addQueryDialog.widget.setFocus(true);
+ return;
+ }
+ if (queryName.equals(DELETE_QUERY)) {
+ deleteQueryDialog.widget.clear();
+ fillQueryList(deleteQueryDialog.widget);
+ deleteQueryDialog.center();
+ return;
+ }
+
+ String idString = queryList.getValue(selected);
+ // don't use CustomHistory, since we want the token to be processed
+ History.newItem(HISTORY_TOKEN + "=" + idString);
+ }
+
+ public void onClick(Widget sender) {
+ if (sender == addQueryDialog.actionButton) {
+ addQueryDialog.hide();
+ JSONObject args = new JSONObject();
+ args.put("name", new JSONString(addQueryDialog.widget.getText()));
+ args.put("url_token", new JSONString(CustomHistory.getLastHistoryToken()));
+ rpcProxy.rpcCall("add_saved_query", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ notifyManager.showMessage("Query saved");
+ populateMainList();
+ }
+ });
+ } else {
+ assert sender == deleteQueryDialog.actionButton;
+ deleteQueryDialog.hide();
+ String idString =
+ deleteQueryDialog.widget.getValue(deleteQueryDialog.widget.getSelectedIndex());
+ JSONObject args = new JSONObject();
+ JSONArray ids = new JSONArray();
+ ids.set(0, new JSONNumber(Integer.parseInt(idString)));
+ args.put("id_list", ids);
+ rpcProxy.rpcCall("delete_saved_queries", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ notifyManager.showMessage("Query deleted");
+ populateMainList();
+ }
+ });
+ }
+ }
+
+ public void onHistoryChanged(Map<String, String> arguments) {
+ final String idString = arguments.get(HISTORY_TOKEN);
+ if (idString == null) {
+ return;
+ }
+
+ JSONObject args = new JSONObject();
+ args.put("id", new JSONNumber(Integer.parseInt(idString)));
+ rpcProxy.rpcCall("get_saved_queries", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ JSONArray queries = result.isArray();
+ if (queries.size() == 0) {
+ notifyManager.showError("No saved query with ID " + idString);
+ return;
+ }
+
+ assert queries.size() == 1;
+ JSONObject query = queries.get(0).isObject();
+ int queryId = (int) query.get("id").isNumber().doubleValue();
+ String token = query.get("url_token").isString().stringValue();
+ // since this is happening asynchronously, the history may have changed, so ensure
+ // it's set back to what it should be.
+ CustomHistory.newItem(HISTORY_TOKEN + "=" + Integer.toString(queryId));
+ CustomHistory.simulateHistoryToken(token);
+ }
+ });
+ }
+}
diff --git a/frontend/client/src/autotest/tko/Spreadsheet.java b/frontend/client/src/autotest/tko/Spreadsheet.java
new file mode 100644
index 0000000..e91423a
--- /dev/null
+++ b/frontend/client/src/autotest/tko/Spreadsheet.java
@@ -0,0 +1,544 @@
+package autotest.tko;
+
+import autotest.common.UnmodifiableSublistView;
+import autotest.common.Utils;
+import autotest.common.ui.RightClickTable;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.IncrementalCommand;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HTMLTable;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.ScrollListener;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.SourcesTableEvents;
+import com.google.gwt.user.client.ui.TableListener;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class Spreadsheet extends Composite implements ScrollListener, TableListener {
+ private static final int MIN_TABLE_SIZE_PX = 90;
+ private static final int WINDOW_BORDER_PX = 15;
+ private static final int SCROLLBAR_FUDGE = 16;
+ private static final String BLANK_STRING = "(empty)";
+ private static final int CELL_PADDING_PX = 2;
+ 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>();
+ private Map<Header, Integer> rowHeaderMap = new HashMap<Header, Integer>();
+ private Map<Header, Integer> columnHeaderMap = new HashMap<Header, Integer>();
+ protected CellInfo[][] dataCells, rowHeaderCells, columnHeaderCells;
+ private RightClickTable rowHeaders = new RightClickTable();
+ private RightClickTable columnHeaders = new RightClickTable();
+ private FlexTable parentTable = new FlexTable();
+ private FragmentedTable dataTable = new FragmentedTable();
+ private int rowsPerIteration;
+ 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);
+ }
+
+ public static interface Header extends List<String> {}
+ public static class HeaderImpl extends ArrayList<String> implements Header {
+ public HeaderImpl() {
+ }
+
+ public HeaderImpl(Collection<? extends String> arg0) {
+ super(arg0);
+ }
+
+ public static Header fromBaseType(List<String> baseType) {
+ return new HeaderImpl(baseType);
+ }
+ }
+
+ public static class CellInfo {
+ public Header row, column;
+ public String contents;
+ public String color;
+ public Integer widthPx, heightPx;
+ public int rowSpan = 1, colSpan = 1;
+ public int testCount = 0;
+
+ 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,
+ rowIndex, rowsPerIteration, true);
+ rowIndex += rowsPerIteration;
+ if (rowIndex > dataCells.length) {
+ state++;
+ }
+ }
+
+ public boolean execute() {
+ switch (state) {
+ case 0:
+ computeRowsPerIteration();
+ computeHeaderCells();
+ break;
+ case 1:
+ renderHeaders();
+ expandRowHeaders();
+ // set main table to match header sizes
+ matchRowHeights(rowHeaders, dataCells);
+ matchColumnWidths(columnHeaders, dataCells);
+ dataTable.setVisible(false);
+ break;
+ case 2:
+ // resize everything to the max dimensions (the window size)
+ fillWindow(false);
+ break;
+ case 3:
+ // render the main data table
+ renderSomeRows();
+ return true;
+ case 4:
+ dataTable.updateBodyElems();
+ dataTable.setVisible(true);
+ break;
+ case 5:
+ // now expand headers as necessary
+ // this can be very slow, so put it in it's own cycle
+ matchRowHeights(dataTable, rowHeaderCells);
+ break;
+ case 6:
+ matchColumnWidths(dataTable, columnHeaderCells);
+ renderHeaders();
+ break;
+ case 7:
+ // shrink the scroller if the table ended up smaller than the window
+ fillWindow(true);
+ DeferredCommand.addCommand(onFinished);
+ return false;
+ }
+
+ state++;
+ return true;
+ }
+ }
+
+ 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.addScrollListener(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);
+ }
+
+ private void setupTableInput(RightClickTable table) {
+ table.sinkRightClickEvents();
+ table.addTableListener(this);
+ }
+
+ protected void killPaddingAndSpacing(HTMLTable table) {
+ table.setCellSpacing(0);
+ table.setCellPadding(0);
+ }
+
+ /*
+ * Wrap a widget with a panel that will clip its contents rather than grow
+ * too much.
+ */
+ protected Panel wrapWithClipper(Widget w) {
+ SimplePanel wrapper = new SimplePanel();
+ wrapper.add(w);
+ 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);
+ assert !headerMap.containsKey(headerObject);
+ 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
+ */
+ public void prepareForData() {
+ dataCells = new CellInfo[rowHeaderValues.size()][columnHeaderValues.size()];
+ }
+
+ public CellInfo getCellInfo(int row, int column) {
+ Header rowHeader = rowHeaderValues.get(row);
+ Header columnHeader = columnHeaderValues.get(column);
+ if (dataCells[row][column] == null) {
+ dataCells[row][column] = new CellInfo(rowHeader, columnHeader, "");
+ }
+ 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.
+ */
+ public void render(IncrementalCommand onFinished) {
+ DeferredCommand.addCommand(new RenderCommand(onFinished));
+ }
+
+ private void renderHeaders() {
+ 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,
+ boolean isRows) {
+ int headerSize = fields.size();
+ String[] lastFieldValue = new String[headerSize];
+ CellInfo[] lastCellInfo = new CellInfo[headerSize];
+ int[] counter = new int[headerSize];
+ boolean newHeader;
+ for (int headerIndex = 0; headerIndex < headerValues.size(); headerIndex++) {
+ Header header = headerValues.get(headerIndex);
+ newHeader = false;
+ for (int fieldIndex = 0; fieldIndex < headerSize; fieldIndex++) {
+ String fieldValue = header.get(fieldIndex);
+ if (newHeader || !fieldValue.equals(lastFieldValue[fieldIndex])) {
+ newHeader = true;
+ Header currentHeader = getSubHeader(header, fieldIndex + 1);
+ String cellContents = formatHeader(fields.get(fieldIndex), fieldValue);
+ CellInfo cellInfo;
+ if (isRows) {
+ cellInfo = new CellInfo(currentHeader, null, cellContents);
+ cells[headerIndex][fieldIndex] = cellInfo;
+ } else {
+ cellInfo = new CellInfo(null, currentHeader, cellContents);
+ cells[fieldIndex][counter[fieldIndex]] = cellInfo;
+ counter[fieldIndex]++;
+ }
+ lastFieldValue[fieldIndex] = fieldValue;
+ lastCellInfo[fieldIndex] = cellInfo;
+ } else {
+ incrementSpan(lastCellInfo[fieldIndex], isRows);
+ }
+ }
+ }
+ }
+
+ private String formatHeader(String field, String value) {
+ if (value.equals("")) {
+ return BLANK_STRING;
+ }
+ value = Utils.escape(value);
+ if (field.equals("kernel")) {
+ // line break after each /, for long paths
+ value = value.replace("/", "/<br>").replace("/<br>/<br>", "//");
+ }
+ return value;
+ }
+
+ private void incrementSpan(CellInfo cellInfo, boolean isRows) {
+ if (isRows) {
+ cellInfo.rowSpan++;
+ } else {
+ cellInfo.colSpan++;
+ }
+ }
+
+ private Header getSubHeader(Header header, int length) {
+ if (length == header.size()) {
+ return header;
+ }
+ List<String> subHeader = new UnmodifiableSublistView<String>(header, 0, length);
+ return new HeaderImpl(subHeader);
+ }
+
+ private void matchRowHeights(HTMLTable from, CellInfo[][] to) {
+ int lastColumn = to[0].length - 1;
+ int rowCount = from.getRowCount();
+ for (int row = 0; row < rowCount; row++) {
+ int height = getRowHeight(from, row);
+ 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;
+ for (int column = 0; column < from.getCellCount(lastFromRow); column++) {
+ int width = getColumnWidth(from, column);
+ 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();
+ if (div == null)
+ return null;
+ String contents = Utils.unescape(div.getInnerHTML());
+ if (contents.equals(BLANK_STRING))
+ contents = "";
+ return contents;
+ }
+
+ public void clear() {
+ rowHeaderValues.clear();
+ columnHeaderValues.clear();
+ rowHeaderMap.clear();
+ 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() +
+ columnHeaders.getOffsetHeight());
+ newHeightPx = adjustMaxDimension(newHeightPx);
+ 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));
+ scrollPanel.setSize(getSizePxString(newWidthPx + SCROLLBAR_FUDGE),
+ getSizePxString(newHeightPx + SCROLLBAR_FUDGE));
+ }
+
+ /**
+ * 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,
+ 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.
+ */
+ protected void expandRowHeaders() {
+ 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;
+ if (cellInfo.row == null) {
+ tdElement = getCellElement(columnHeaders, 0, getColumnPosition(cellInfo.column));
+ } else if (cellInfo.column == null) {
+ tdElement = getCellElement(rowHeaders, getRowPosition(cellInfo.row), 0);
+ } else {
+ 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() -
+ 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() -
+ TD_BORDER_PX;
+ }
+
+ /**
+ * Update floating headers.
+ */
+ public void onScroll(Widget widget, int scrollLeft, int scrollTop) {
+ setColumnHeadersOffset(-scrollLeft);
+ setRowHeadersOffset(-scrollTop);
+ }
+
+ protected void setRowHeadersOffset(int offset) {
+ rowHeaders.getElement().getStyle().setPropertyPx("top", offset);
+ }
+
+ protected void setColumnHeadersOffset(int offset) {
+ columnHeaders.getElement().getStyle().setPropertyPx("left", offset);
+ }
+
+ public void onCellClicked(SourcesTableEvents sender, int row, int column) {
+ if (listener == null)
+ return;
+
+ CellInfo[][] cells;
+ if (sender == rowHeaders) {
+ cells = rowHeaderCells;
+ column = adjustRowHeaderColumnIndex(row, column);
+ }
+ else if (sender == columnHeaders) {
+ cells = columnHeaderCells;
+ }
+ else {
+ assert sender == dataTable;
+ cells = dataCells;
+ }
+ CellInfo cell = cells[row][column];
+ if (cell == null || cell.isEmpty())
+ return; // don't report clicks on empty cells
+ listener.onCellClicked(cell);
+ }
+
+ /**
+ * 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.
+ */
+ private int adjustRowHeaderColumnIndex(int row, int column) {
+ for (int i = 0; i < rowFields.size(); i++) {
+ if (rowHeaderCells[row][i] != null) {
+ return i + column;
+ }
+ }
+
+ throw new RuntimeException("Failed to find non-null cell");
+ }
+
+ public void setListener(SpreadsheetListener listener) {
+ this.listener = listener;
+ }
+
+ public void setHighlighted(CellInfo cell, boolean highlighted) {
+ Element cellElement = getCellElement(cell);
+ if (highlighted) {
+ cellElement.setClassName(HIGHLIGHTED_CLASS);
+ } else {
+ cellElement.setClassName("");
+ }
+ }
+}
diff --git a/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java b/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java
new file mode 100644
index 0000000..974d868
--- /dev/null
+++ b/frontend/client/src/autotest/tko/SpreadsheetDataProcessor.java
@@ -0,0 +1,179 @@
+package autotest.tko;
+
+import autotest.common.table.DataSource.DataCallback;
+import autotest.common.ui.NotifyManager;
+import autotest.tko.Spreadsheet.CellInfo;
+import autotest.tko.Spreadsheet.Header;
+
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.IncrementalCommand;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SpreadsheetDataProcessor implements DataCallback {
+ 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;
+
+ private Header rowFields;
+ private Header columnFields;
+ private Command onFinished;
+ private Duration timer;
+
+ public class TooManyCellsError extends Exception {
+ public int cellCount;
+
+ public TooManyCellsError(int cellCount) {
+ super();
+ this.cellCount = cellCount;
+ }
+ }
+
+ private class ProcessDataCommand implements IncrementalCommand {
+ private int state = 0;
+ private JSONArray counts;
+ private int currentRow = 0;
+
+ public ProcessDataCommand(JSONArray counts) {
+ this.counts = counts;
+ }
+
+ public void processSomeRows() {
+ for (int i = 0; i < ROWS_PROCESSED_PER_ITERATION; i++, currentRow++) {
+ if (currentRow == counts.size()) {
+ state++;
+ return;
+ }
+ processRow(counts.get(currentRow).isObject());
+ }
+ }
+
+ public boolean execute() {
+ switch (state) {
+ case 0:
+ notifyManager.setLoading(true);
+ numTotalTests = 0;
+ try {
+ processHeaders();
+ } catch (TooManyCellsError exc) {
+ notifyManager.showError("Resulting spreadsheet contains " + exc.cellCount + " cells, " +
+ "exceeding maximum " + MAX_CELL_COUNT);
+ finalizeCommand();
+ return false;
+ }
+ break;
+ case 1:
+ spreadsheet.prepareForData();
+ break;
+ case 2:
+ processSomeRows();
+ return true;
+ case 3:
+ // we must make the spreadsheet visible before rendering, or size computations
+ // won't work correctly
+ spreadsheet.setVisible(true);
+ break;
+ case 4:
+ spreadsheet.render(this);
+ state++;
+ return false; // render will re-add us to the command queue
+ case 5:
+ logTimer("Rendering");
+ finalizeCommand();
+ return false;
+ }
+
+ state++;
+ return true;
+ }
+
+ private void finalizeCommand() {
+ notifyManager.setLoading(false);
+ onFinished.execute();
+ }
+ }
+
+ public SpreadsheetDataProcessor(Spreadsheet spreadsheet, TestGroupDataSource dataSource) {
+ this.spreadsheet = spreadsheet;
+ this.dataSource = dataSource;
+ }
+
+ public void processHeaders() throws TooManyCellsError {
+ spreadsheet.setHeaderFields(rowFields, columnFields);
+ List<List<String>> rowHeaderValues = dataSource.getHeaderGroupValues(0);
+ List<List<String>> columnHeaderValues = dataSource.getHeaderGroupValues(1);
+ int totalCells = rowHeaderValues.size() * columnHeaderValues.size();
+ if (totalCells > MAX_CELL_COUNT) {
+ throw new TooManyCellsError(totalCells);
+ }
+ for (List<String> header : rowHeaderValues) {
+ spreadsheet.addRowHeader(header);
+ }
+ for (List<String> header : columnHeaderValues) {
+ spreadsheet.addColumnHeader(header);
+ }
+ }
+
+ private void processRow(JSONObject group) {
+ JSONArray headerIndices = group.get("header_indices").isArray();
+ int row = (int) headerIndices.get(0).isNumber().doubleValue();
+ int column = (int) headerIndices.get(1).isNumber().doubleValue();
+ CellInfo cellInfo = spreadsheet.getCellInfo(row, column);
+ StatusSummary statusSummary = StatusSummary.getStatusSummary(group);
+ numTotalTests += statusSummary.getTotal();
+ cellInfo.contents = statusSummary.formatStatusCounts();
+ cellInfo.color = statusSummary.getColor();
+ cellInfo.testCount = statusSummary.getTotal();
+ }
+
+ public void refresh(Header rowFields, Header columnFields, String condition,
+ Command onFinished) {
+ timer = new Duration();
+ this.onFinished = onFinished;
+ this.rowFields = rowFields;
+ this.columnFields = columnFields;
+ List<List<String>> headerGroups = new ArrayList<List<String>>();
+ headerGroups.add(rowFields);
+ headerGroups.add(columnFields);
+ dataSource.setHeaderGroups(headerGroups);
+ JSONObject params = TkoUtils.getConditionParams(condition);
+ dataSource.updateData(params, this);
+ }
+
+ public void onGotData(int totalCount) {
+ dataSource.getPage(null, null, null, this);
+ }
+
+ public void handlePage(JSONArray data) {
+ logTimer("Server response");
+ if (data.size() == 0) {
+ notifyManager.showMessage("No results for query");
+ onFinished.execute();
+ return;
+ }
+
+ DeferredCommand.addCommand(new ProcessDataCommand(data));
+ }
+
+ private void logTimer(String message) {
+ notifyManager.log(message + ": " + timer.elapsedMillis() + " ms");
+ timer = new Duration();
+ }
+
+ public int getNumTotalTests() {
+ return numTotalTests;
+ }
+
+ public void onError(JSONObject errorObject) {
+ onFinished.execute();
+ }
+}
diff --git a/frontend/client/src/autotest/tko/SpreadsheetSelectionManager.java b/frontend/client/src/autotest/tko/SpreadsheetSelectionManager.java
new file mode 100644
index 0000000..9f8e034
--- /dev/null
+++ b/frontend/client/src/autotest/tko/SpreadsheetSelectionManager.java
@@ -0,0 +1,91 @@
+package autotest.tko;
+
+import autotest.common.Utils;
+import autotest.tko.Spreadsheet.CellInfo;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+// TODO: hopefully some of this could be combined with autotest.common.table.SelectionManager using
+// generics
+// TODO: get rid of header selection
+public class SpreadsheetSelectionManager {
+ private Spreadsheet spreadsheet;
+ private Collection<CellInfo> selectedCells = new HashSet<CellInfo>();
+ private SpreadsheetSelectionListener listener;
+
+ public static interface SpreadsheetSelectionListener {
+ public void onCellsSelected(List<CellInfo> cells);
+ public void onCellsDeselected(List<CellInfo> cells);
+ }
+
+ public SpreadsheetSelectionManager(Spreadsheet spreadsheet,
+ SpreadsheetSelectionListener listener) {
+ this.spreadsheet = spreadsheet;
+ this.listener = listener;
+ }
+
+ public void toggleSelected(CellInfo cell) {
+ if (selectedCells.contains(cell)) {
+ deselectCell(cell);
+ notifyDeselected(Utils.wrapObjectWithList(cell));
+ } else {
+ selectCell(cell);
+ notifySelected(Utils.wrapObjectWithList(cell));
+ }
+ }
+
+ private void selectCell(CellInfo cell) {
+ selectedCells.add(cell);
+ spreadsheet.setHighlighted(cell, true);
+ }
+
+ private void deselectCell(CellInfo cell) {
+ selectedCells.remove(cell);
+ spreadsheet.setHighlighted(cell, false);
+ }
+
+ public List<CellInfo> getSelectedCells() {
+ return new ArrayList<CellInfo>(selectedCells);
+ }
+
+ public boolean isEmpty() {
+ return selectedCells.isEmpty();
+ }
+
+ public void clearSelection() {
+ List<CellInfo> cells = getSelectedCells();
+ for (CellInfo cell : cells) {
+ deselectCell(cell);
+ }
+ notifyDeselected(cells);
+ }
+
+ public void selectAll() {
+ List<CellInfo> selectedCells = new ArrayList<CellInfo>();
+ for (CellInfo[] row : spreadsheet.dataCells) {
+ for (CellInfo cell : row) {
+ if (cell == null || cell.isEmpty()) {
+ continue;
+ }
+ selectCell(cell);
+ selectedCells.add(cell);
+ }
+ }
+ notifySelected(selectedCells);
+ }
+
+ private void notifyDeselected(List<CellInfo> cells) {
+ if (listener != null) {
+ listener.onCellsDeselected(cells);
+ }
+ }
+
+ private void notifySelected(List<CellInfo> selectedCells) {
+ if (listener != null) {
+ listener.onCellsSelected(selectedCells);
+ }
+ }
+}
diff --git a/frontend/client/src/autotest/tko/SpreadsheetView.java b/frontend/client/src/autotest/tko/SpreadsheetView.java
new file mode 100644
index 0000000..8a141e3
--- /dev/null
+++ b/frontend/client/src/autotest/tko/SpreadsheetView.java
@@ -0,0 +1,485 @@
+package autotest.tko;
+
+import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.UnmodifiableSublistView;
+import autotest.common.Utils;
+import autotest.common.ui.ContextMenu;
+import autotest.common.ui.NotifyManager;
+import autotest.common.ui.RightClickTable;
+import autotest.common.ui.SimpleHyperlink;
+import autotest.common.ui.TableActionsPanel;
+import autotest.common.ui.DoubleListSelector.Item;
+import autotest.common.ui.TableActionsPanel.TableActionsListener;
+import autotest.tko.Spreadsheet.CellInfo;
+import autotest.tko.Spreadsheet.Header;
+import autotest.tko.Spreadsheet.HeaderImpl;
+import autotest.tko.Spreadsheet.SpreadsheetListener;
+import autotest.tko.TkoUtils.FieldInfo;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.WindowResizeListener;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.MenuBar;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SpreadsheetView extends ConditionTabView
+ implements SpreadsheetListener, TableActionsListener {
+ public static final String DEFAULT_ROW = "kernel";
+ public static final String DEFAULT_COLUMN = "platform";
+
+ private static enum DrilldownType {DRILLDOWN_ROW, DRILLDOWN_COLUMN, DRILLDOWN_BOTH}
+
+ private static JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+ private static JsonRpcProxy afeRpcProxy = JsonRpcProxy.getProxy(JsonRpcProxy.AFE_URL);
+ private SpreadsheetViewListener listener;
+ protected Header currentRowFields;
+ protected Header currentColumnFields;
+ protected Map<String,String[]> drilldownMap = new HashMap<String,String[]>();
+
+ private HeaderSelect rowSelect = new HeaderSelect();
+ private HeaderSelect columnSelect = new HeaderSelect();
+ private CheckBox showIncomplete = new CheckBox("Show incomplete tests");
+ private Button queryButton = new Button("Query");
+ private TestGroupDataSource dataSource = TestGroupDataSource.getStatusCountDataSource();
+ private Spreadsheet spreadsheet = new Spreadsheet();
+ private SpreadsheetDataProcessor spreadsheetProcessor =
+ new SpreadsheetDataProcessor(spreadsheet, dataSource);
+ private SpreadsheetSelectionManager selectionManager =
+ new SpreadsheetSelectionManager(spreadsheet, null);
+ private TableActionsPanel actionsPanel = new TableActionsPanel(this, false);
+ private RootPanel jobCompletionPanel;
+ private boolean currentShowIncomplete;
+ private boolean notYetQueried = true;
+
+ public static interface SpreadsheetViewListener extends TestSelectionListener {
+ public void onSwitchToTable(boolean isTriageView);
+ }
+
+ public SpreadsheetView(SpreadsheetViewListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public String getElementId() {
+ return "spreadsheet_view";
+ }
+
+ @Override
+ public void initialize() {
+ dataSource.setSkipNumResults(true);
+
+ currentRowFields = new HeaderImpl();
+ currentRowFields.add(DEFAULT_ROW);
+ currentColumnFields = new HeaderImpl();
+ currentColumnFields.add(DEFAULT_COLUMN);
+
+ for (FieldInfo fieldInfo : TkoUtils.getFieldList("group_fields")) {
+ rowSelect.addItem(fieldInfo.name, fieldInfo.field);
+ columnSelect.addItem(fieldInfo.name, fieldInfo.field);
+ }
+ updateWidgets();
+
+ queryButton.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ doQuery();
+ updateHistory();
+ }
+ });
+
+ spreadsheet.setVisible(false);
+ spreadsheet.setListener(this);
+
+ SimpleHyperlink swapLink = new SimpleHyperlink("swap");
+ swapLink.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ Header newRows = getSelectedHeader(columnSelect);
+ setSelectedHeader(columnSelect, getSelectedHeader(rowSelect));
+ setSelectedHeader(rowSelect, newRows);
+ }
+ });
+
+ Panel queryControlPanel = new VerticalPanel();
+ queryControlPanel.add(showIncomplete);
+ queryControlPanel.add(queryButton);
+
+ RootPanel.get("ss_row_select").add(rowSelect);
+ RootPanel.get("ss_column_select").add(columnSelect);
+ RootPanel.get("ss_swap").add(swapLink);
+ RootPanel.get("ss_query_controls").add(queryControlPanel);
+ RootPanel.get("ss_actions").add(actionsPanel);
+ RootPanel.get("ss_spreadsheet").add(spreadsheet);
+ jobCompletionPanel = RootPanel.get("ss_job_completion");
+
+ Window.addWindowResizeListener(new WindowResizeListener() {
+ public void onWindowResized(int width, int height) {
+ if(spreadsheet.isVisible())
+ spreadsheet.fillWindow(true);
+ }
+ });
+
+ setupDrilldownMap();
+ }
+
+ protected TestSet getWholeTableTestSet() {
+ boolean isSingleTest = spreadsheetProcessor.getNumTotalTests() == 1;
+ return new ConditionTestSet(isSingleTest, commonPanel.getSavedCondition());
+ }
+
+ protected void setupDrilldownMap() {
+ drilldownMap.put("platform", new String[] {"hostname", "test_name"});
+ drilldownMap.put("hostname", new String[] {"job_tag", "status"});
+ drilldownMap.put("job_tag", new String[] {"job_tag"});
+
+ drilldownMap.put("kernel", new String[] {"test_name", "status"});
+ drilldownMap.put("test_name", new String[] {"job_name", "job_tag"});
+
+ drilldownMap.put("status", new String[] {"reason", "job_tag"});
+ drilldownMap.put("reason", new String[] {"job_tag"});
+
+ drilldownMap.put("job_owner", new String[] {"job_name", "job_tag"});
+ drilldownMap.put("job_name", new String[] {"job_tag"});
+
+ drilldownMap.put("test_finished_time", new String[] {"status", "job_tag"});
+ drilldownMap.put("DATE(test_finished_time)",
+ new String[] {"test_finished_time", "job_tag"});
+ }
+
+ private Header getSelectedHeader(HeaderSelect list) {
+ Header selectedFields = new HeaderImpl();
+ for (Item item : list.getSelectedItems()) {
+ selectedFields.add(item.value);
+ }
+ return selectedFields;
+ }
+
+ protected void setSelectedHeader(HeaderSelect list, Header fields) {
+ list.selectItemsByValue(fields);
+ }
+
+ @Override
+ public void refresh() {
+ notYetQueried = false;
+ spreadsheet.setVisible(false);
+ selectionManager.clearSelection();
+ spreadsheet.clear();
+ setJobCompletionHtml(" ");
+
+ String condition = commonPanel.getSavedCondition();
+ if (!currentShowIncomplete) {
+ if (!condition.equals("")) {
+ condition = "(" + condition + ") AND ";
+ }
+ condition += "status != 'RUNNING'";
+ }
+ final String finalCondition = condition;
+
+ setLoading(true);
+ spreadsheetProcessor.refresh(currentRowFields, currentColumnFields, finalCondition,
+ new Command() {
+ public void execute() {
+ if (isJobFilteringCondition(finalCondition)) {
+ showCompletionPercentage(finalCondition);
+ } else {
+ setLoading(false);
+ }
+ }
+ });
+ }
+
+ public void doQuery() {
+ Header rows = getSelectedHeader(rowSelect), columns = getSelectedHeader(columnSelect);
+ if (rows.isEmpty() || columns.isEmpty()) {
+ NotifyManager.getInstance().showError("You must select row and column fields");
+ return;
+ }
+ currentRowFields = getSelectedHeader(rowSelect);
+ currentColumnFields = getSelectedHeader(columnSelect);
+ currentShowIncomplete = showIncomplete.isChecked();
+ saveCondition();
+ refresh();
+ }
+
+ private void showCompletionPercentage(String condition) {
+ rpcProxy.rpcCall("get_job_ids", TkoUtils.getConditionParams(condition),
+ new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ finishShowCompletionPercentage(result.isArray());
+ setLoading(false);
+ }
+
+ @Override
+ public void onError(JSONObject errorObject) {
+ super.onError(errorObject);
+ setLoading(false);
+ }
+ });
+ }
+
+ private void finishShowCompletionPercentage(JSONArray jobIds) {
+ final int jobCount = jobIds.size();
+ if (jobCount == 0) {
+ return;
+ }
+
+ JSONObject args = new JSONObject();
+ args.put("job__id__in", jobIds);
+ afeRpcProxy.rpcCall("get_hqe_percentage_complete", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ int percentage = (int) (result.isNumber().doubleValue() * 100);
+ StringBuilder message = new StringBuilder("Matching ");
+ if (jobCount == 1) {
+ message.append("job is ");
+ } else {
+ message.append("jobs are ");
+ }
+ message.append(percentage);
+ message.append("% complete");
+ setJobCompletionHtml(message.toString());
+ }
+ });
+ }
+
+ private void setJobCompletionHtml(String html) {
+ jobCompletionPanel.clear();
+ jobCompletionPanel.add(new HTML(html));
+ }
+
+ private boolean isJobFilteringCondition(String condition) {
+ return condition.indexOf("job_tag") != -1;
+ }
+
+ public void onCellClicked(CellInfo cellInfo) {
+ Event event = Event.getCurrentEvent();
+ TestSet testSet = getTestSet(cellInfo);
+ DrilldownType drilldownType = getDrilldownType(cellInfo);
+ if (RightClickTable.isRightClick(event)) {
+ if (!selectionManager.isEmpty()) {
+ testSet = getTestSet(selectionManager.getSelectedCells());
+ drilldownType = DrilldownType.DRILLDOWN_BOTH;
+ }
+ ContextMenu menu = getContextMenu(testSet, drilldownType);
+ menu.showAtWindow(event.getClientX(), event.getClientY());
+ return;
+ }
+
+ if (event.getCtrlKey()) {
+ selectionManager.toggleSelected(cellInfo);
+ return;
+ }
+
+ if (testSet.isSingleTest()) {
+ TkoUtils.getTestId(testSet, listener);
+ return;
+ }
+
+ doDrilldown(testSet,
+ getDefaultDrilldownRow(drilldownType),
+ getDefaultDrilldownColumn(drilldownType));
+ }
+
+ private DrilldownType getDrilldownType(CellInfo cellInfo) {
+ if (cellInfo.row == null) {
+ // column header
+ return DrilldownType.DRILLDOWN_COLUMN;
+ }
+ if (cellInfo.column == null) {
+ // row header
+ return DrilldownType.DRILLDOWN_ROW;
+ }
+ return DrilldownType.DRILLDOWN_BOTH;
+ }
+
+ private TestSet getTestSet(CellInfo cellInfo) {
+ boolean isSingleTest = cellInfo.testCount == 1;
+ ConditionTestSet testSet = new ConditionTestSet(isSingleTest,
+ commonPanel.getSavedCondition());
+
+ if (cellInfo.row != null) {
+ setSomeFields(testSet, currentRowFields, cellInfo.row);
+ }
+ if (cellInfo.column != null) {
+ setSomeFields(testSet, currentColumnFields, cellInfo.column);
+ }
+ return testSet;
+ }
+
+ private void setSomeFields(ConditionTestSet testSet, Header allFields, Header values) {
+ List<String> usedFields = new UnmodifiableSublistView<String>(allFields, 0, values.size());
+ testSet.setFields(usedFields, values);
+ }
+
+ private TestSet getTestSet(List<CellInfo> cells) {
+ CompositeTestSet tests = new CompositeTestSet();
+ for (CellInfo cell : cells) {
+ tests.add(getTestSet(cell));
+ }
+ return tests;
+ }
+
+ private void doDrilldown(TestSet tests, String newRowField, String newColumnField) {
+ commonPanel.refineCondition(tests);
+ currentRowFields = HeaderImpl.fromBaseType(Utils.wrapObjectWithList(newRowField));
+ currentColumnFields = HeaderImpl.fromBaseType(Utils.wrapObjectWithList(newColumnField));
+ updateWidgets();
+ doQuery();
+ updateHistory();
+ }
+
+ private String getDefaultDrilldownRow(DrilldownType type) {
+ return getDrilldownRows(type)[0];
+ }
+
+ private String getDefaultDrilldownColumn(DrilldownType type) {
+ return getDrilldownColumns(type)[0];
+ }
+
+ private ContextMenu getContextMenu(final TestSet tests, DrilldownType drilldownType) {
+ TestContextMenu menu = new TestContextMenu(tests, listener);
+
+ if (!menu.addViewDetailsIfSingleTest()) {
+ MenuBar drilldownMenu = menu.addSubMenuItem("Drill down");
+ fillDrilldownMenu(tests, drilldownType, drilldownMenu);
+ }
+
+ menu.addItem("View in table", new Command() {
+ public void execute() {
+ switchToTable(tests, false);
+ }
+ });
+ menu.addItem("Triage failures", new Command() {
+ public void execute() {
+ switchToTable(tests, true);
+ }
+ });
+
+ menu.addLabelItems();
+ return menu;
+ }
+
+ private void fillDrilldownMenu(final TestSet tests, DrilldownType drilldownType, MenuBar menu) {
+ for (final String rowField : getDrilldownRows(drilldownType)) {
+ for (final String columnField : getDrilldownColumns(drilldownType)) {
+ if (rowField.equals(columnField)) {
+ continue;
+ }
+ menu.addItem(rowField + " vs. " + columnField, new Command() {
+ public void execute() {
+ doDrilldown(tests, rowField, columnField);
+ }
+ });
+ }
+ }
+ }
+
+ private String[] getDrilldownFields(Header fields, DrilldownType type,
+ DrilldownType otherType) {
+ String lastField = fields.get(fields.size() - 1);
+ if (type == otherType) {
+ return new String[] {lastField};
+ } else {
+ return drilldownMap.get(lastField);
+ }
+ }
+
+ private String[] getDrilldownRows(DrilldownType type) {
+ return getDrilldownFields(currentRowFields, type, DrilldownType.DRILLDOWN_COLUMN);
+ }
+
+ private String[] getDrilldownColumns(DrilldownType type) {
+ return getDrilldownFields(currentColumnFields, type, DrilldownType.DRILLDOWN_ROW);
+ }
+
+ private void updateWidgets() {
+ setSelectedHeader(rowSelect, currentRowFields);
+ setSelectedHeader(columnSelect, currentColumnFields);
+ showIncomplete.setChecked(currentShowIncomplete);
+ }
+
+ @Override
+ protected Map<String, String> getHistoryArguments() {
+ Map<String, String> arguments = super.getHistoryArguments();
+ if (!notYetQueried) {
+ arguments.put("row", headerToString(currentRowFields));
+ arguments.put("column", headerToString(currentColumnFields));
+ arguments.put("show_incomplete", Boolean.toString(currentShowIncomplete));
+ commonPanel.addHistoryArguments(arguments);
+ }
+ return arguments;
+ }
+
+ private String headerToString(Header header) {
+ return Utils.joinStrings(",", header);
+ }
+
+ @Override
+ public void handleHistoryArguments(Map<String, String> arguments) {
+ super.handleHistoryArguments(arguments);
+ commonPanel.handleHistoryArguments(arguments);
+
+ String rows = arguments.get("row"), columns = arguments.get("column");
+ currentRowFields = HeaderImpl.fromBaseType(Arrays.asList(rows.split(",")));
+ currentColumnFields = HeaderImpl.fromBaseType(Arrays.asList(columns.split(",")));
+ currentShowIncomplete = Boolean.valueOf(arguments.get("show_incomplete"));
+
+ updateWidgets();
+ }
+
+ @Override
+ protected void fillDefaultHistoryValues(Map<String, String> arguments) {
+ Utils.setDefaultValue(arguments, "row", DEFAULT_ROW);
+ Utils.setDefaultValue(arguments, "column", DEFAULT_COLUMN);
+ Utils.setDefaultValue(arguments, "show_incomplete", Boolean.toString(currentShowIncomplete));
+ }
+
+ private void switchToTable(final TestSet tests, boolean isTriageView) {
+ commonPanel.refineCondition(tests);
+ listener.onSwitchToTable(isTriageView);
+ }
+
+ public ContextMenu getActionMenu() {
+ TestSet tests;
+ if (selectionManager.isEmpty()) {
+ tests = getWholeTableTestSet();
+ } else {
+ tests = getTestSet(selectionManager.getSelectedCells());
+ }
+ return getContextMenu(tests, DrilldownType.DRILLDOWN_BOTH);
+ }
+
+ public void onSelectAll(boolean ignored) {
+ selectionManager.selectAll();
+ }
+
+ public void onSelectNone() {
+ selectionManager.clearSelection();
+ }
+
+ @Override
+ protected boolean hasFirstQueryOccurred() {
+ return !notYetQueried;
+ }
+
+ private void setLoading(boolean loading) {
+ queryButton.setEnabled(!loading);
+ NotifyManager.getInstance().setLoading(loading);
+ }
+}
diff --git a/frontend/client/src/autotest/tko/StatusSummary.java b/frontend/client/src/autotest/tko/StatusSummary.java
new file mode 100644
index 0000000..ea2ab74
--- /dev/null
+++ b/frontend/client/src/autotest/tko/StatusSummary.java
@@ -0,0 +1,84 @@
+// Copyright 2008 Google Inc. All Rights Reserved.
+
+package autotest.tko;
+
+import com.google.gwt.json.client.JSONObject;
+
+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"),
+ };
+
+ 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
+
+ /**
+ * 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);
+ 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 formatStatusCounts() {
+ String text = passed + " / " + complete;
+ if (incomplete > 0) {
+ text += "<br>" + incomplete + " incomplete";
+ }
+ return text;
+ }
+
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/frontend/client/src/autotest/tko/TableRenderer.java b/frontend/client/src/autotest/tko/TableRenderer.java
new file mode 100644
index 0000000..deb94b8
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TableRenderer.java
@@ -0,0 +1,101 @@
+package autotest.tko;
+
+import autotest.tko.Spreadsheet.CellInfo;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.HTMLTable;
+
+
+public class TableRenderer {
+ // 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,
+ int startRow, int maxRows, boolean renderNull) {
+ StringBuffer htmlBuffer= new StringBuffer();
+ htmlBuffer.append("<table><tbody>");
+ for (int rowIndex = startRow; rowIndex < startRow + maxRows && rowIndex < rows.length;
+ rowIndex++) {
+ CellInfo[] row = rows[rowIndex];
+ htmlBuffer.append("<tr>");
+ for (CellInfo cell : row) {
+ if (cell == null && renderNull) {
+ 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.rowSpan > 1) {
+ tdAttributes += attributeString("rowspan", Integer.toString(cell.rowSpan));
+ }
+ if (cell.colSpan > 1) {
+ tdAttributes += attributeString("colspan", Integer.toString(cell.colSpan));
+ }
+
+ if (cell.widthPx != null) {
+ divStyle += SIZE_PREFIX + "width: " + cell.widthPx + "px; ";
+ }
+ if (cell.heightPx != null) {
+ divStyle += SIZE_PREFIX + "height: " + cell.heightPx + "px; ";
+ }
+ if (!divStyle.equals("")) {
+ divAttributes += attributeString("style", divStyle);
+ }
+ if (cell.isEmpty()) {
+ divAttributes += attributeString("class", NONCLICKABLE_CLASS);
+ }
+
+ htmlBuffer.append("<td " + tdAttributes + ">");
+ htmlBuffer.append("<div " + divAttributes + ">");
+ htmlBuffer.append(cell.contents);
+ htmlBuffer.append("</div></td>");
+ }
+ }
+ htmlBuffer.append("</tr>");
+ }
+ htmlBuffer.append("</tbody></table>");
+
+ renderBody(tableObject, htmlBuffer.toString());
+ }
+
+ public void renderRows(HTMLTable tableObject, CellInfo[][] rows, boolean renderNull) {
+ TkoUtils.clearDomChildren(tableObject.getElement()); // remove existing tbodies
+ renderRowsAndAppend(tableObject, rows, 0, rows.length, renderNull);
+ }
+
+ public void renderRows(HTMLTable tableObject, CellInfo[][] rows) {
+ renderRows(tableObject, rows, true);
+ }
+
+ private void renderBody(HTMLTable tableObject, String html) {
+ // 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.
+ */
+ protected native void setBodyElement(HTMLTable table, Element newBody) /*-{
+ table.@com.google.gwt.user.client.ui.HTMLTable::bodyElem = newBody;
+ }-*/;
+
+}
diff --git a/frontend/client/src/autotest/tko/TableView.java b/frontend/client/src/autotest/tko/TableView.java
new file mode 100644
index 0000000..fa86f35
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TableView.java
@@ -0,0 +1,466 @@
+package autotest.tko;
+
+import autotest.common.Utils;
+import autotest.common.table.DataTable;
+import autotest.common.table.DynamicTable;
+import autotest.common.table.RpcDataSource;
+import autotest.common.table.SelectionManager;
+import autotest.common.table.SimpleFilter;
+import autotest.common.table.TableDecorator;
+import autotest.common.table.DataSource.SortDirection;
+import autotest.common.table.DataSource.SortSpec;
+import autotest.common.table.DataTable.TableWidgetFactory;
+import autotest.common.table.DynamicTable.DynamicTableListener;
+import autotest.common.table.SelectionManager.SelectionListener;
+import autotest.common.ui.ContextMenu;
+import autotest.common.ui.DoubleListSelector;
+import autotest.common.ui.NotifyManager;
+import autotest.common.ui.RightClickTable;
+import autotest.common.ui.TableActionsPanel;
+import autotest.common.ui.DoubleListSelector.Item;
+import autotest.common.ui.TableActionsPanel.TableActionsListener;
+import autotest.tko.TkoUtils.FieldInfo;
+
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+public class TableView extends ConditionTabView
+ implements DynamicTableListener, TableActionsListener, SelectionListener,
+ ClickListener, TableWidgetFactory {
+ private static final int ROWS_PER_PAGE = 30;
+ private static final String[] DEFAULT_COLUMNS =
+ {"test_idx", "test_name", "job_tag", "hostname", "status"};
+ private static final String[] TRIAGE_GROUP_COLUMNS =
+ {"test_name", "status", "reason"};
+ private static final SortSpec[] TRIAGE_SORT_SPECS = {
+ new SortSpec("test_name", SortDirection.ASCENDING),
+ new SortSpec("status", SortDirection.ASCENDING),
+ new SortSpec("reason", SortDirection.ASCENDING),
+ };
+ private static final String COUNT_NAME = "Count in group";
+ private static final String STATUS_COUNTS_NAME = "Test pass rate";
+
+ private TestSelectionListener listener;
+
+ private DynamicTable table;
+ private TableDecorator tableDecorator;
+ private SelectionManager selectionManager;
+ private SimpleFilter sqlConditionFilter = new SimpleFilter();
+ private RpcDataSource testDataSource = new TestViewDataSource();
+ private TestGroupDataSource groupDataSource = TestGroupDataSource.getTestGroupDataSource();
+ private DoubleListSelector columnSelect = new DoubleListSelector();
+ private CheckBox groupCheckbox = new CheckBox("Group by these columns and show counts");
+ private CheckBox statusGroupCheckbox =
+ new CheckBox("Group by these columns and show pass rates");
+ private TableActionsPanel actionsPanel = new TableActionsPanel(this, true);
+ private Button queryButton = new Button("Query");
+
+ private boolean isTestGroupingEnabled = false, isStatusCountEnabled = false;
+ private List<String> fields = new ArrayList<String>();
+ private List<SortSpec> tableSorts = new ArrayList<SortSpec>();
+ private Map<String, String> fieldNames = new HashMap<String, String>();
+
+ public TableView(TestSelectionListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public String getElementId() {
+ return "table_view";
+ }
+
+ @Override
+ public void initialize() {
+ for (FieldInfo fieldInfo : TkoUtils.getFieldList("all_fields")) {
+ fieldNames.put(fieldInfo.field, fieldInfo.name);
+ columnSelect.addItem(fieldInfo.name, fieldInfo.field);
+ }
+ fieldNames.put(TestGroupDataSource.GROUP_COUNT_FIELD, COUNT_NAME);
+ fieldNames.put(DataTable.WIDGET_COLUMN, STATUS_COUNTS_NAME);
+
+ selectColumns(DEFAULT_COLUMNS);
+ saveOptions(false);
+
+ queryButton.addClickListener(this);
+ groupCheckbox.addClickListener(this);
+ statusGroupCheckbox.addClickListener(this);
+
+ Panel columnPanel = new VerticalPanel();
+ columnPanel.add(columnSelect);
+ columnPanel.add(groupCheckbox);
+ columnPanel.add(statusGroupCheckbox);
+
+ RootPanel.get("table_column_select").add(columnPanel);
+ RootPanel.get("table_actions").add(actionsPanel);
+ RootPanel.get("table_query_controls").add(queryButton);
+ }
+
+ private void selectColumns(String[] columns) {
+ columnSelect.deselectAll();
+ for(String field : columns) {
+ columnSelect.selectItemByValue(field);
+ }
+ }
+
+ public void setupJobTriage() {
+ // easier if we ensure it's deselected and then select it
+ selectColumns(TRIAGE_GROUP_COLUMNS);
+ statusGroupCheckbox.setChecked(false);
+ groupCheckbox.setChecked(true);
+ updateCheckboxes();
+
+ // need to copy it so we can mutate it
+ tableSorts = new ArrayList<SortSpec>(Arrays.asList(TRIAGE_SORT_SPECS));
+ }
+
+ private void createTable() {
+ int numColumns = fields.size();
+ String[][] columns = new String[numColumns][2];
+ for (int i = 0; i < numColumns; i++) {
+ String field = fields.get(i);
+ columns[i][0] = field;
+ columns[i][1] = fieldNames.get(field);
+ }
+
+ RpcDataSource dataSource = testDataSource;
+ if (isAnyGroupingEnabled()) {
+ if (isTestGroupingEnabled) {
+ groupDataSource = TestGroupDataSource.getTestGroupDataSource();
+ } else {
+ groupDataSource = TestGroupDataSource.getStatusCountDataSource();
+ }
+
+ updateGroupColumns();
+ dataSource = groupDataSource;
+ } else {
+ dataSource = testDataSource;
+ }
+
+ table = new DynamicTable(columns, dataSource);
+ table.addFilter(sqlConditionFilter);
+ table.setRowsPerPage(ROWS_PER_PAGE);
+ table.makeClientSortable();
+ table.setClickable(true);
+ table.sinkRightClickEvents();
+ table.addListener(this);
+ table.setWidgetFactory(this);
+ restoreTableSorting();
+
+ tableDecorator = new TableDecorator(table);
+ tableDecorator.addPaginators();
+ Panel tablePanel = RootPanel.get("table_table");
+ tablePanel.clear();
+ tablePanel.add(tableDecorator);
+
+ selectionManager = new SelectionManager(table, false);
+ selectionManager.addListener(this);
+ }
+
+ private void saveOptions(boolean doSaveCondition) {
+ if (doSaveCondition) {
+ saveCondition();
+ }
+
+ fields.clear();
+ for (Item item : columnSelect.getSelectedItems()) {
+ fields.add(item.value);
+ }
+
+ isTestGroupingEnabled = groupCheckbox.isChecked();
+ isStatusCountEnabled = statusGroupCheckbox.isChecked();
+ }
+
+ private void updateGroupColumns() {
+ List<String> groupFields = new ArrayList<String>();
+ for (String field : fields) {
+ if (!field.equals(TestGroupDataSource.GROUP_COUNT_FIELD) &&
+ !field.equals(DataTable.WIDGET_COLUMN)) {
+ groupFields.add(field);
+ }
+ }
+ groupDataSource.setGroupColumns(groupFields.toArray(new String[0]));
+ }
+
+ private void saveTableSorting() {
+ if (table != null) {
+ // we need our own copy so we can modify it later
+ tableSorts = new ArrayList<SortSpec>(table.getSortSpecs());
+ }
+ }
+
+ private void restoreTableSorting() {
+ // remove sorts on columns that we no longer have
+ for (Iterator<SortSpec> i = tableSorts.iterator(); i.hasNext();) {
+ if (!fields.contains(i.next().getField())) {
+ i.remove();
+ }
+ }
+ if (tableSorts.isEmpty()) {
+ // default to sorting on the first column
+ SortSpec sortSpec = new SortSpec(fields.get(0), SortDirection.ASCENDING);
+ tableSorts = Arrays.asList(new SortSpec[] {sortSpec});
+ }
+
+ for (ListIterator<SortSpec> i = tableSorts.listIterator(tableSorts.size());
+ i.hasPrevious();) {
+ SortSpec sortSpec = i.previous();
+ table.sortOnColumn(sortSpec.getField(), sortSpec.getDirection());
+ }
+ }
+
+ @Override
+ public void refresh() {
+ createTable();
+ String sqlCondition = commonPanel.getSavedCondition();
+ sqlConditionFilter.setParameter("extra_where", new JSONString(sqlCondition));
+ table.refresh();
+ }
+
+ public void doQuery() {
+ if (columnSelect.getSelectedItemCount() == 0) {
+ NotifyManager.getInstance().showError("You must select columns");
+ return;
+ }
+ saveOptions(true);
+ refresh();
+ }
+
+ public void onRowClicked(int rowIndex, JSONObject row) {
+ Event event = Event.getCurrentEvent();
+ TestSet testSet = getTestSet(row);
+ if (RightClickTable.isRightClick(event)) {
+ if (selectionManager.getSelectedObjects().size() > 0) {
+ testSet = getTestSet(selectionManager.getSelectedObjects());
+ }
+ ContextMenu menu = getContextMenu(testSet);
+ menu.showAtWindow(event.getClientX(), event.getClientY());
+ return;
+ }
+
+ if (event.getCtrlKey()) {
+ selectionManager.toggleSelected(row);
+ return;
+ }
+
+ if (isAnyGroupingEnabled()) {
+ doDrilldown(testSet);
+ } else {
+ TkoUtils.getTestId(testSet, listener);
+ }
+ }
+
+ 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() {
+ doDrilldown(testSet);
+ }
+ });
+ }
+
+ menu.addLabelItems();
+ return menu;
+ }
+
+ private void doDrilldown(TestSet testSet) {
+ commonPanel.refineCondition(testSet);
+ uncheckBothCheckboxes();
+ updateCheckboxes();
+ selectColumns(DEFAULT_COLUMNS);
+ doQuery();
+ updateHistory();
+ }
+
+ private void uncheckBothCheckboxes() {
+ groupCheckbox.setChecked(false);
+ statusGroupCheckbox.setChecked(false);
+ }
+
+ private TestSet getTestSet(JSONObject row) {
+ ConditionTestSet testSet;
+ if (isAnyGroupingEnabled()) {
+ testSet = new ConditionTestSet(false, commonPanel.getSavedCondition());
+ for (String field : fields) {
+ if (field.equals(TestGroupDataSource.GROUP_COUNT_FIELD) ||
+ field.equals(DataTable.WIDGET_COLUMN)) {
+ continue;
+ }
+ testSet.setField(field, Utils.jsonToString(row.get(field)));
+ }
+ } else {
+ testSet = new ConditionTestSet(true);
+ testSet.setField("test_idx",
+ Utils.jsonToString(row.get("test_idx")));
+ }
+ return testSet;
+ }
+
+ private TestSet getTestSet(Collection<JSONObject> selectedObjects) {
+ CompositeTestSet compositeSet = new CompositeTestSet();
+ for (JSONObject row : selectedObjects) {
+ compositeSet.add(getTestSet(row));
+ }
+ return compositeSet;
+ }
+
+ public void onTableRefreshed() {
+ selectionManager.refreshSelection();
+
+ saveTableSorting();
+ updateHistory();
+ }
+
+ private void updateCheckboxes() {
+ ensureItemRemoved(COUNT_NAME);
+ ensureItemRemoved(STATUS_COUNTS_NAME);
+ groupCheckbox.setEnabled(true);
+ statusGroupCheckbox.setEnabled(true);
+
+ if (groupCheckbox.isChecked()) {
+ columnSelect.addReadonlySelectedItem(COUNT_NAME, TestGroupDataSource.GROUP_COUNT_FIELD);
+ statusGroupCheckbox.setEnabled(false);
+ } else if (statusGroupCheckbox.isChecked()) {
+ columnSelect.addReadonlySelectedItem(STATUS_COUNTS_NAME, DataTable.WIDGET_COLUMN);
+ groupCheckbox.setEnabled(false);
+ }
+ }
+
+ private void ensureItemRemoved(String itemName) {
+ try {
+ columnSelect.removeItem(itemName);
+ } catch (IllegalArgumentException exc) {}
+ }
+
+ public ContextMenu getActionMenu() {
+ TestSet tests;
+ if (selectionManager.isEmpty()) {
+ tests = getWholeTableSet();
+ } else {
+ tests = getTestSet(selectionManager.getSelectedObjects());
+ }
+ return getContextMenu(tests);
+ }
+
+ private ConditionTestSet getWholeTableSet() {
+ return new ConditionTestSet(false, commonPanel.getSavedCondition());
+ }
+
+ public void onSelectAll(boolean visibleOnly) {
+ if (visibleOnly) {
+ selectionManager.selectVisible();
+ } else {
+ selectionManager.selectAll();
+ }
+ }
+
+ public void onSelectNone() {
+ selectionManager.deselectAll();
+ }
+
+ @Override
+ protected Map<String, String> getHistoryArguments() {
+ Map<String, String> arguments = super.getHistoryArguments();
+ if (table != null) {
+ arguments.put("columns", Utils.joinStrings(",", fields));
+ arguments.put("sort", Utils.joinStrings(",", tableSorts));
+ commonPanel.addHistoryArguments(arguments);
+ }
+ return arguments;
+ }
+
+ @Override
+ public void handleHistoryArguments(Map<String, String> arguments) {
+ super.handleHistoryArguments(arguments);
+
+ handleSortString(arguments.get("sort"));
+
+ columnSelect.deselectAll();
+ uncheckBothCheckboxes();
+ String[] columns = arguments.get("columns").split(",");
+ for (String column : columns) {
+ if (column.equals(TestGroupDataSource.GROUP_COUNT_FIELD)) {
+ groupCheckbox.setChecked(true);
+ } else if (column.equals(DataTable.WIDGET_COLUMN)) {
+ statusGroupCheckbox.setChecked(true);
+ } else {
+ columnSelect.selectItemByValue(column);
+ }
+ }
+ updateCheckboxes();
+ saveOptions(true);
+ }
+
+ @Override
+ protected void fillDefaultHistoryValues(Map<String, String> arguments) {
+ Utils.setDefaultValue(arguments, "sort", DEFAULT_COLUMNS[0]);
+ Utils.setDefaultValue(arguments, "columns",
+ Utils.joinStrings(",", Arrays.asList(DEFAULT_COLUMNS)));
+
+ }
+
+ private void handleSortString(String sortString) {
+ tableSorts.clear();
+ String[] components = sortString.split(",");
+ for (String component : components) {
+ tableSorts.add(SortSpec.fromString(component));
+ }
+ }
+
+ public void onAdd(Collection<JSONObject> objects) {
+ selectionManager.refreshSelection();
+ }
+
+ public void onRemove(Collection<JSONObject> objects) {
+ selectionManager.refreshSelection();
+ }
+
+ public void onClick(Widget sender) {
+ if (sender == queryButton) {
+ doQuery();
+ updateHistory();
+ } else if (sender == groupCheckbox || sender == statusGroupCheckbox) {
+ updateCheckboxes();
+ }
+ }
+
+ private boolean isAnyGroupingEnabled() {
+ return isTestGroupingEnabled || isStatusCountEnabled;
+ }
+
+ public Widget createWidget(int row, int cell, JSONObject rowObject) {
+ assert isStatusCountEnabled;
+ StatusSummary statusSummary = StatusSummary.getStatusSummary(rowObject);
+ SimplePanel panel = new SimplePanel();
+ panel.add(new HTML(statusSummary.formatStatusCounts()));
+ panel.getElement().getStyle().setProperty("backgroundColor",
+ statusSummary.getColor());
+ return panel;
+ }
+
+ @Override
+ protected boolean hasFirstQueryOccurred() {
+ return table != null;
+ }
+}
diff --git a/frontend/client/src/autotest/tko/TestContextMenu.java b/frontend/client/src/autotest/tko/TestContextMenu.java
new file mode 100644
index 0000000..83185c7
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TestContextMenu.java
@@ -0,0 +1,42 @@
+package autotest.tko;
+
+import autotest.common.ui.ContextMenu;
+
+import com.google.gwt.user.client.Command;
+
+public class TestContextMenu extends ContextMenu {
+ private static TestLabelManager labelManager = TestLabelManager.getManager();
+ private TestSet tests;
+ private TestSelectionListener listener;
+
+ public TestContextMenu(TestSet tests, TestSelectionListener listener) {
+ this.tests = tests;
+ this.listener = listener;
+ }
+
+ public boolean addViewDetailsIfSingleTest() {
+ if (!tests.isSingleTest()) {
+ return false;
+ }
+
+ addItem("View test details", new Command() {
+ public void execute() {
+ TkoUtils.getTestId(tests, listener);
+ }
+ });
+ return true;
+ }
+
+ public void addLabelItems() {
+ addItem("Add label", new Command() {
+ public void execute() {
+ labelManager.handleAddLabels(tests.getCondition());
+ }
+ });
+ addItem("Remove label", new Command() {
+ public void execute() {
+ labelManager.handleRemoveLabels(tests.getCondition());
+ }
+ });
+ }
+}
diff --git a/frontend/client/src/autotest/tko/TestDetailView.java b/frontend/client/src/autotest/tko/TestDetailView.java
new file mode 100644
index 0000000..3853a2b
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TestDetailView.java
@@ -0,0 +1,220 @@
+package autotest.tko;
+
+import autotest.common.JsonRpcCallback;
+import autotest.common.Utils;
+import autotest.common.ui.DetailView;
+import autotest.common.ui.NotifyManager;
+
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DisclosureEvent;
+import com.google.gwt.user.client.ui.DisclosureHandler;
+import com.google.gwt.user.client.ui.DisclosurePanel;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.RootPanel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class TestDetailView extends DetailView {
+ private static final int NO_TEST_ID = -1;
+
+ private int testId = NO_TEST_ID;
+ private String jobTag;
+ private List<LogFileViewer> logFileViewers = new ArrayList<LogFileViewer>();
+
+ private Panel logPanel;
+
+ private class LogFileViewer extends Composite implements DisclosureHandler, RequestCallback {
+ private static final String FAILED_TEXT = "Failed to retrieve log";
+ private DisclosurePanel panel;
+ private String logFilePath;
+
+ public LogFileViewer(String logFilePath, String logFileName) {
+ this.logFilePath = logFilePath;
+ panel = new DisclosurePanel(logFileName);
+ panel.addEventHandler(this);
+ panel.addStyleName("log-file-panel");
+ initWidget(panel);
+ }
+
+ public void onOpen(DisclosureEvent event) {
+ RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, getLogUrl());
+ try {
+ builder.sendRequest("", this);
+ setStatusText("Loading...");
+ } catch (RequestException exc) {
+ onRequestFailure();
+ }
+ }
+
+ private String getLogUrl() {
+ return Utils.getLogsURL(jobTag + "/" + logFilePath);
+ }
+
+ private void setLogText(String text) {
+ panel.clear();
+ Label label = new Label(text);
+ label.setStyleName("log-file-text");
+ panel.add(label);
+ }
+
+ private void setStatusText(String status) {
+ panel.clear();
+ panel.add(new HTML("<i>" + status + "</i>"));
+ }
+
+ public void onClose(DisclosureEvent event) {}
+
+ public void onError(Request request, Throwable exception) {
+ onRequestFailure();
+ }
+
+ private void onRequestFailure() {
+ setStatusText(FAILED_TEXT);
+ }
+
+ public void onResponseReceived(Request request, Response response) {
+ if (response.getStatusCode() != 200) {
+ onRequestFailure();
+ return;
+ }
+
+ String logText = response.getText();
+ if (logText.equals("")) {
+ setStatusText("Log file is empty");
+ } else {
+ setLogText(logText);
+ }
+ }
+ }
+
+ @Override
+ public void initialize() {
+ super.initialize();
+
+ logPanel = new FlowPanel();
+ RootPanel.get("td_log_files").add(logPanel);
+ }
+
+ private void addLogViewers(String testName) {
+ logPanel.clear();
+ addLogFileViewer(testName + "/debug/stdout", "Test stdout");
+ addLogFileViewer(testName + "/debug/stderr", "Test stderr");
+ addLogFileViewer("status.log", "Job status log");
+ addLogFileViewer("debug/autoserv.stdout", "Job autoserv stdout");
+ addLogFileViewer("debug/autoserv.stderr", "Job autoserv stderr");
+ }
+
+ private void addLogFileViewer(String logPath, String logName) {
+ LogFileViewer viewer = new LogFileViewer(logPath, logName);
+ logFileViewers.add(viewer);
+ logPanel.add(viewer);
+ }
+
+ @Override
+ protected void fetchData() {
+ JSONObject params = new JSONObject();
+ params.put("test_idx", new JSONNumber(testId));
+ rpcProxy.rpcCall("get_test_views", params, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ JSONObject test;
+ try {
+ test = Utils.getSingleValueFromArray(result.isArray()).isObject();
+ }
+ catch (IllegalArgumentException exc) {
+ NotifyManager.getInstance().showError("No such job found");
+ resetPage();
+ return;
+ }
+
+ String testName = test.get("test_name").isString().stringValue();
+ jobTag = test.get("job_tag").isString().stringValue();
+
+ showText(testName, "td_test");
+ showText(jobTag, "td_job_tag");
+ showField(test, "job_name", "td_job_name");
+ showField(test, "status", "td_status");
+ showField(test, "reason", "td_reason");
+ showField(test, "test_started_time", "td_test_started");
+ showField(test, "test_finished_time", "td_test_finished");
+ showField(test, "hostname", "td_hostname");
+ showField(test, "platform", "td_platform");
+ showField(test, "kernel", "td_kernel");
+
+ DOM.getElementById("td_view_logs_link").setAttribute("href",
+ Utils.getLogsURL(jobTag));
+ addLogViewers(testName);
+
+ displayObjectData("Test " + testName + " (job " + jobTag + ")");
+ }
+
+ @Override
+ public void onError(JSONObject errorObject) {
+ super.onError(errorObject);
+ resetPage();
+ }
+ });
+ }
+
+ @Override
+ protected void setObjectId(String id) {
+ try {
+ testId = Integer.parseInt(id);
+ }
+ catch (NumberFormatException exc) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ @Override
+ protected String getObjectId() {
+ if (testId == NO_TEST_ID) {
+ return NO_OBJECT;
+ }
+ return Integer.toString(testId);
+ }
+
+ @Override
+ protected String getDataElementId() {
+ return "td_data";
+ }
+
+ @Override
+ protected String getFetchControlsElementId() {
+ return "td_fetch_controls";
+ }
+
+ @Override
+ protected String getNoObjectText() {
+ return "No test selected";
+ }
+
+ @Override
+ protected String getTitleElementId() {
+ return "td_title";
+ }
+
+ @Override
+ public String getElementId() {
+ return "test_detail_view";
+ }
+
+ @Override
+ public void display() {
+ super.display();
+ CommonPanel.getPanel().setConditionVisible(false);
+ }
+}
diff --git a/frontend/client/src/autotest/tko/TestGroupDataSource.java b/frontend/client/src/autotest/tko/TestGroupDataSource.java
new file mode 100644
index 0000000..a16292e
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TestGroupDataSource.java
@@ -0,0 +1,105 @@
+package autotest.tko;
+
+import autotest.common.JSONArrayList;
+import autotest.common.Utils;
+import autotest.common.table.RpcDataSource;
+
+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.List;
+
+class TestGroupDataSource extends RpcDataSource {
+ private static final String NUM_GROUPS_RPC = "get_num_groups";
+ private static final String GROUP_COUNTS_RPC = "get_group_counts";
+ private static final String STATUS_COUNTS_RPC = "get_status_counts";
+ public static final String GROUP_COUNT_FIELD = "group_count";
+ public static final String PASS_COUNT_FIELD = "pass_count";
+ public static final String COMPLETE_COUNT_FIELD = "complete_count";
+ public static final String INCOMPLETE_COUNT_FIELD = "incomplete_count";
+
+ private JSONArray groupByFields;
+ private JSONArray headerGroups;
+ private JSONArray headerGroupValues;
+ private boolean skipNumResults = false;
+
+ public static TestGroupDataSource getTestGroupDataSource() {
+ return new TestGroupDataSource(GROUP_COUNTS_RPC);
+ }
+
+ public static TestGroupDataSource getStatusCountDataSource() {
+ return new TestGroupDataSource(STATUS_COUNTS_RPC);
+ }
+
+ // force construction to go through above factory methods
+ private TestGroupDataSource(String getDataMethod) {
+ super(getDataMethod, NUM_GROUPS_RPC);
+ }
+
+ @Override
+ public void updateData(JSONObject params, DataCallback callback) {
+ params = Utils.copyJSONObject(params);
+ params.put("group_by", groupByFields);
+ if (headerGroups != null) {
+ params.put("header_groups", headerGroups);
+ }
+
+ if (skipNumResults) {
+ filterParams = params;
+ numResults = 0;
+ callback.onGotData(numResults);
+ } else {
+ super.updateData(params, callback);
+ }
+ }
+
+ @Override
+ /**
+ * Process the groups, which come simply as lists, into JSONObjects.
+ */
+ protected JSONArray handleJsonResult(JSONValue result) {
+ JSONObject resultObject = result.isObject();
+ headerGroupValues = resultObject.get("header_values").isArray();
+ return resultObject.get("groups").isArray();
+ }
+
+ public void setGroupColumns(String[] columns) {
+ groupByFields = new JSONArray();
+ for (String field : columns) {
+ groupByFields.set(groupByFields.size(), new JSONString(field));
+ }
+ headerGroups = null;
+ }
+
+ public void setHeaderGroups(List<List<String>> headerFieldGroups) {
+ groupByFields = new JSONArray();
+ headerGroups = new JSONArray();
+ for (List<String> headerGroup : headerFieldGroups) {
+ headerGroups.set(headerGroups.size(), Utils.stringsToJSON(headerGroup));
+ for(String field : headerGroup) {
+ groupByFields.set(groupByFields.size(), new JSONString(field));
+ }
+ }
+ }
+
+ /**
+ * Get a list of values for the header group with the given index, as specified to
+ * setHeaderGroups().
+ */
+ public List<List<String>> getHeaderGroupValues(int groupIndex) {
+ JSONArray headerList = headerGroupValues.get(groupIndex).isArray();
+ List<List<String>> headerValueList = new ArrayList<List<String>>();
+ for (JSONArray header : new JSONArrayList<JSONArray>(headerList)) {
+ headerValueList.add(Arrays.asList(Utils.JSONtoStrings(header)));
+ }
+ return headerValueList;
+ }
+
+ public void setSkipNumResults(boolean skipNumResults) {
+ this.skipNumResults = skipNumResults;
+ }
+}
diff --git a/frontend/client/src/autotest/tko/TestLabelManager.java b/frontend/client/src/autotest/tko/TestLabelManager.java
new file mode 100644
index 0000000..1ed4a31
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TestLabelManager.java
@@ -0,0 +1,215 @@
+package autotest.tko;
+
+import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.SimpleCallback;
+import autotest.common.StaticDataRepository;
+import autotest.common.Utils;
+import autotest.common.ui.NotifyManager;
+import autotest.common.ui.SimpleHyperlink;
+
+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.Button;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.DialogBox;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.StackPanel;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+public class TestLabelManager implements ClickListener {
+ private static final String ADD_TEXT = "Add label";
+ private static final String REMOVE_TEXT = "Remove label";
+ private static final int STACK_SELECT = 0, STACK_CREATE = 1;
+
+ private static final TestLabelManager theInstance = new TestLabelManager();
+
+ private static final JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+ private static NotifyManager notifyManager = NotifyManager.getInstance();
+ private static StaticDataRepository staticData = StaticDataRepository.getRepository();
+
+ private DialogBox selectLabelDialog = new DialogBox(false, true); // modal
+ private ListBox labelList = new ListBox();
+ private TextBox newLabelName = new TextBox();
+ private SimpleHyperlink createLabelLink, cancelCreateLink;
+ private StackPanel stack = new StackPanel();
+ private Button submitButton = new Button(), cancelButton = new Button("Cancel");
+
+ private String currentTestCondition;
+
+
+ private TestLabelManager() {
+ createLabelLink = new SimpleHyperlink("Create new label");
+ cancelCreateLink = new SimpleHyperlink("Cancel create label");
+ ClickListener linkListener = new ClickListener() {
+ public void onClick(Widget sender) {
+ if (sender == createLabelLink) {
+ stack.showStack(STACK_CREATE);
+ } else {
+ stack.showStack(STACK_SELECT);
+ }
+ }
+ };
+ createLabelLink.addClickListener(linkListener);
+ cancelCreateLink.addClickListener(linkListener);
+
+ Panel selectPanel = new VerticalPanel();
+ selectPanel.add(new HTML("Select label:"));
+ selectPanel.add(labelList);
+ selectPanel.add(createLabelLink);
+ stack.add(selectPanel);
+
+ Panel createPanel = new VerticalPanel();
+ createPanel.add(new HTML("Enter label name:"));
+ createPanel.add(newLabelName);
+ createPanel.add(cancelCreateLink);
+ stack.add(createPanel);
+
+ Panel buttonPanel = new HorizontalPanel();
+ buttonPanel.add(submitButton);
+ buttonPanel.add(cancelButton);
+
+ Panel dialogPanel = new VerticalPanel();
+ dialogPanel.add(stack);
+ dialogPanel.add(buttonPanel);
+ selectLabelDialog.add(dialogPanel);
+
+ submitButton.addClickListener(this);
+ cancelButton.addClickListener(this);
+ }
+
+ public static TestLabelManager getManager() {
+ return theInstance;
+ }
+
+ private void setLinksVisible(boolean visible) {
+ createLabelLink.setVisible(visible);
+ cancelCreateLink.setVisible(visible);
+ }
+
+ public void handleAddLabels(String testCondition) {
+ currentTestCondition = testCondition;
+ newLabelName.setText("");
+
+ String[] labels = Utils.JSONObjectsToStrings(staticData.getData("test_labels").isArray(),
+ "name");
+ if (labels.length == 0) {
+ setLinksVisible(false);
+ stack.showStack(STACK_CREATE);
+ } else {
+ setLinksVisible(true);
+ stack.showStack(STACK_SELECT);
+ populateLabelList(labels);
+ }
+ showDialog(ADD_TEXT);
+ }
+
+ public void handleRemoveLabels(String testCondition) {
+ currentTestCondition = testCondition;
+
+ JSONObject args = new JSONObject();
+ args.put("extra_where", new JSONString(currentTestCondition));
+ rpcProxy.rpcCall("get_test_labels_for_tests", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ String[] labels = Utils.JSONObjectsToStrings(result.isArray(), "name");
+ if (labels.length == 0) {
+ notifyManager.showMessage("No labels on selected tests");
+ return;
+ }
+ populateLabelList(labels);
+ setLinksVisible(false);
+ stack.showStack(STACK_SELECT);
+ showDialog(REMOVE_TEXT);
+ }
+ });
+ }
+
+ private void showDialog(String actionText) {
+ submitButton.setText(actionText);
+ selectLabelDialog.setText(actionText);
+ selectLabelDialog.center();
+ }
+
+ private void populateLabelList(String[] labels) {
+ labelList.clear();
+ for (String label : labels) {
+ labelList.addItem(label);
+ }
+ }
+
+ public void onClick(Widget sender) {
+ selectLabelDialog.hide();
+
+ if (sender == cancelButton) {
+ return;
+ }
+
+ if (submitButton.getText().equals(ADD_TEXT)) {
+ SimpleCallback doAdd = new SimpleCallback() {
+ public void doCallback(Object source) {
+ addOrRemoveLabel((String) source, true);
+ }
+ };
+
+ if (stack.getSelectedIndex() == STACK_CREATE) {
+ addLabel(newLabelName.getText(), doAdd);
+ } else {
+ doAdd.doCallback(getSelectedLabel());
+ }
+ } else {
+ assert (submitButton.getText().equals(REMOVE_TEXT));
+ addOrRemoveLabel(getSelectedLabel(), false);
+ }
+ }
+
+ private String getSelectedLabel() {
+ return labelList.getItemText(labelList.getSelectedIndex());
+ }
+
+ private void addLabel(final String name, final SimpleCallback onFinished) {
+ JSONObject args = new JSONObject();
+ args.put("name", new JSONString(name));
+ rpcProxy.rpcCall("add_test_label", args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ onFinished.doCallback(name);
+ }
+ });
+ updateLabels();
+ }
+
+ private void addOrRemoveLabel(String label, boolean add) {
+ String rpcMethod;
+ if (add) {
+ rpcMethod = "test_label_add_tests";
+ } else {
+ rpcMethod = "test_label_remove_tests";
+ }
+
+ JSONObject args = new JSONObject();
+ args.put("label_id", new JSONString(label));
+ args.put("extra_where", new JSONString(currentTestCondition));
+ rpcProxy.rpcCall(rpcMethod, args, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ notifyManager.showMessage("Labels modified successfully");
+ }
+ });
+ }
+
+ private void updateLabels() {
+ rpcProxy.rpcCall("get_test_labels", null, new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ staticData.setData("test_labels", result);
+ }
+ });
+ }
+}
diff --git a/frontend/client/src/autotest/tko/TestSelectionListener.java b/frontend/client/src/autotest/tko/TestSelectionListener.java
new file mode 100644
index 0000000..27f3b07
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TestSelectionListener.java
@@ -0,0 +1,5 @@
+package autotest.tko;
+
+interface TestSelectionListener {
+ public void onSelectTest(int testId);
+}
diff --git a/frontend/client/src/autotest/tko/TestSet.java b/frontend/client/src/autotest/tko/TestSet.java
new file mode 100644
index 0000000..3e450f0
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TestSet.java
@@ -0,0 +1,6 @@
+package autotest.tko;
+
+interface TestSet {
+ public String getCondition();
+ public boolean isSingleTest();
+}
diff --git a/frontend/client/src/autotest/tko/TestViewDataSource.java b/frontend/client/src/autotest/tko/TestViewDataSource.java
new file mode 100644
index 0000000..0e0e645
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TestViewDataSource.java
@@ -0,0 +1,26 @@
+package autotest.tko;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONValue;
+
+import autotest.common.JSONArrayList;
+import autotest.common.table.RpcDataSource;
+
+class TestViewDataSource extends RpcDataSource {
+ public TestViewDataSource() {
+ super("get_test_views", "get_num_test_views");
+ }
+
+ /**
+ * Add 'id' field, needed by SelectionManager.
+ */
+ @Override
+ protected JSONArray handleJsonResult(JSONValue result) {
+ JSONArray objects = super.handleJsonResult(result);
+ for (JSONObject object : new JSONArrayList<JSONObject>(objects)) {
+ object.put("id", object.get("test_idx"));
+ }
+ return objects;
+ }
+}
diff --git a/frontend/client/src/autotest/tko/TkoClient.java b/frontend/client/src/autotest/tko/TkoClient.java
new file mode 100644
index 0000000..ba40551
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TkoClient.java
@@ -0,0 +1,74 @@
+package autotest.tko;
+
+import autotest.common.CustomHistory;
+import autotest.common.JsonRpcProxy;
+import autotest.common.StaticDataRepository;
+import autotest.common.ui.CustomTabPanel;
+import autotest.common.ui.NotifyManager;
+import autotest.tko.SpreadsheetView.SpreadsheetViewListener;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.user.client.ui.RootPanel;
+
+public class TkoClient implements EntryPoint, SpreadsheetViewListener {
+ private CommonPanel commonPanel;
+ private SpreadsheetView spreadsheetView;
+ private TableView tableView;
+ private GraphingView graphingView;
+ private TestDetailView detailView;
+
+ private CustomTabPanel mainTabPanel = new CustomTabPanel();
+ private SavedQueriesControl savedQueriesControl;
+
+ public void onModuleLoad() {
+ JsonRpcProxy.setDefaultUrl(JsonRpcProxy.TKO_URL);
+
+ NotifyManager.getInstance().initialize();
+
+ StaticDataRepository.getRepository().refresh(
+ new StaticDataRepository.FinishedCallback() {
+ public void onFinished() {
+ finishLoading();
+ }
+ });
+ }
+
+ protected void finishLoading() {
+ commonPanel = CommonPanel.getPanel();
+ spreadsheetView = new SpreadsheetView(this);
+ tableView = new TableView(this);
+ graphingView = new GraphingView();
+ detailView = new TestDetailView();
+
+ mainTabPanel.getCommonAreaPanel().add(commonPanel);
+ mainTabPanel.addTabView(spreadsheetView);
+ mainTabPanel.addTabView(tableView);
+ mainTabPanel.addTabView(graphingView);
+ mainTabPanel.addTabView(detailView);
+
+ savedQueriesControl = new SavedQueriesControl();
+ mainTabPanel.getOtherWidgetsPanel().add(savedQueriesControl);
+
+ final RootPanel tabsRoot = RootPanel.get("tabs");
+ tabsRoot.add(mainTabPanel);
+ CustomHistory.processInitialToken();
+ mainTabPanel.initialize();
+ commonPanel.initialize();
+ tabsRoot.removeStyleName("hidden");
+ }
+
+ public void onSwitchToTable(boolean isTriageView) {
+ tableView.ensureInitialized();
+ if (isTriageView) {
+ tableView.setupJobTriage();
+ }
+ tableView.doQuery();
+ mainTabPanel.selectTabView(tableView);
+ }
+
+ public void onSelectTest(int testId) {
+ detailView.ensureInitialized();
+ detailView.updateObjectId(Integer.toString(testId));
+ mainTabPanel.selectTabView(detailView);
+ }
+}
diff --git a/frontend/client/src/autotest/tko/TkoUtils.java b/frontend/client/src/autotest/tko/TkoUtils.java
new file mode 100644
index 0000000..dc47911
--- /dev/null
+++ b/frontend/client/src/autotest/tko/TkoUtils.java
@@ -0,0 +1,70 @@
+package autotest.tko;
+
+import autotest.common.JSONArrayList;
+import autotest.common.JsonRpcCallback;
+import autotest.common.JsonRpcProxy;
+import autotest.common.StaticDataRepository;
+
+import com.google.gwt.dom.client.Element;
+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.List;
+
+public class TkoUtils {
+ private static StaticDataRepository staticData = StaticDataRepository.getRepository();
+ private static JsonRpcProxy rpcProxy = JsonRpcProxy.getProxy();
+
+ public static class FieldInfo {
+ public String field;
+ public String name;
+
+ public FieldInfo(String field, String name) {
+ this.field = field;
+ this.name = name;
+ }
+ }
+
+ public static List<FieldInfo> getFieldList(String listName) {
+ JSONArray fieldArray = staticData.getData(listName).isArray();
+ List<FieldInfo> fields = new ArrayList<FieldInfo>();
+ for (JSONArray fieldTuple : new JSONArrayList<JSONArray>(fieldArray)) {
+ String fieldName = fieldTuple.get(0).isString().stringValue();
+ String field = fieldTuple.get(1).isString().stringValue();
+ fields.add(new FieldInfo(field, fieldName));
+ }
+ return fields;
+ }
+
+ protected static JSONObject getConditionParams(String condition) {
+ JSONObject params = new JSONObject();
+ params.put("extra_where", new JSONString(condition));
+ return params;
+ }
+
+ protected static void getTestId(TestSet test, final TestSelectionListener listener) {
+ rpcProxy.rpcCall("get_test_views", getConditionParams(test.getCondition()),
+ new JsonRpcCallback() {
+ @Override
+ public void onSuccess(JSONValue result) {
+ // just take the first result (there could be more than one due to
+ // a rare and harmless race condition)
+ JSONObject testView = result.isArray().get(0).isObject();
+ int testId = (int) testView.get("test_idx").isNumber().doubleValue();
+ listener.onSelectTest(testId);
+ }
+ });
+ }
+
+ protected static void clearDomChildren(Element elem) {
+ Element child = elem.getFirstChildElement();
+ while (child != null) {
+ Element nextChild = child.getNextSiblingElement();
+ elem.removeChild(child);
+ child = nextChild;
+ }
+ }
+}
diff --git a/new_tko/common.py b/new_tko/common.py
new file mode 100644
index 0000000..9941b19
--- /dev/null
+++ b/new_tko/common.py
@@ -0,0 +1,8 @@
+import os, sys
+dirname = os.path.dirname(sys.modules[__name__].__file__)
+autotest_dir = os.path.abspath(os.path.join(dirname, ".."))
+client_dir = os.path.join(autotest_dir, "client")
+sys.path.insert(0, client_dir)
+import setup_modules
+sys.path.pop(0)
+setup_modules.setup(base_path=autotest_dir, root_module_name="autotest_lib")
diff --git a/new_tko/manage.py b/new_tko/manage.py
new file mode 100644
index 0000000..5e78ea9
--- /dev/null
+++ b/new_tko/manage.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/new_tko/settings.py b/new_tko/settings.py
new file mode 100644
index 0000000..3501b86
--- /dev/null
+++ b/new_tko/settings.py
@@ -0,0 +1,99 @@
+# Django settings for new_tko project.
+
+import common
+from autotest_lib.client.common_lib import global_config
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'mysql_old' # 'postgresql_psycopg2', 'postgresql',
+ # 'mysql', 'sqlite3' or 'ado_mssql'.
+DATABASE_PORT = '' # Set to empty string for default.
+ # Not used with sqlite3.
+
+c = global_config.global_config
+_section = 'TKO'
+DATABASE_HOST = c.get_config_value(_section, "host")
+# Or path to database file if using sqlite3.
+DATABASE_NAME = c.get_config_value(_section, "database")
+# The following not used with sqlite3.
+DATABASE_USER = c.get_config_value(_section, "user")
+DATABASE_PASSWORD = c.get_config_value(_section, "password")
+
+DATABASE_READONLY_USER = c.get_config_value(_section, "readonly_user",
+ default=DATABASE_USER)
+DATABASE_READONLY_PASSWORD = c.get_config_value(_section, "readonly_password",
+ default=DATABASE_PASSWORD)
+
+# prefix applied to all URLs - useful if requests are coming through apache,
+# and you need this app to coexist with others
+URL_PREFIX = 'new_tko/server/'
+
+# Local time zone for this installation. Choices can be found here:
+# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
+# although not all variations may be possible on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Los_Angeles'
+
+# Language code for this installation. All choices can be found here:
+# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
+# http://blogs.law.harvard.edu/tech/stories/storyReader$15
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT.
+# Example: "http://media.lawrence.com"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 't5h^o%emw-1#ga4tvcb82)8irk^w5kyy348osow#$=&3c%60@r'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.load_template_source',
+ 'django.template.loaders.app_directories.load_template_source',
+# 'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'autotest_lib.frontend.apache_auth.GetApacheUserMiddleware',
+ 'django.middleware.doc.XViewMiddleware',
+)
+
+ROOT_URLCONF = 'new_tko.urls'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+INSTALLED_APPS = (
+ 'new_tko.tko',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+)
diff --git a/new_tko/tko/models.py b/new_tko/tko/models.py
new file mode 100644
index 0000000..4bb9a59
--- /dev/null
+++ b/new_tko/tko/models.py
@@ -0,0 +1,339 @@
+from django.db import models as dbmodels, connection
+from django.utils import datastructures
+from autotest_lib.frontend.afe import model_logic, readonly_connection
+
+class TempManager(model_logic.ExtendedManager):
+ _GROUP_COUNT_NAME = 'group_count'
+
+ def _get_key_unless_is_function(self, field):
+ if '(' in field:
+ return field
+ return self._get_key_on_this_table(field)
+
+
+ def _get_field_names(self, fields):
+ return [self._get_key_unless_is_function(field) for field in fields]
+
+
+ def _get_group_query_sql(self, query, group_by, extra_select_fields):
+ group_fields = self._get_field_names(group_by)
+ if query._distinct:
+ pk_field = self._get_key_on_this_table(self.model._meta.pk.name)
+ count_sql = 'COUNT(DISTINCT %s)' % pk_field
+ else:
+ count_sql = 'COUNT(1)'
+ select_fields = (group_fields +
+ [count_sql + ' AS ' + self._GROUP_COUNT_NAME] +
+ extra_select_fields)
+
+ # add the count field and all group fields to the query selects, so
+ # they'll be sortable and Django won't mess with any of them
+ #for field in group_fields + [self._GROUP_COUNT_NAME]:
+ # query._select[field] = ''
+ query._select[self._GROUP_COUNT_NAME] = count_sql
+
+ # Inject the GROUP_BY clause into the query by adding it to the end of
+ # the queries WHERE clauses. We need it to come before the ORDER BY and
+ # LIMIT clauses.
+ num_real_where_clauses = len(query._where)
+ query._where.append('GROUP BY ' + ', '.join(group_fields))
+ _, where, params = query._get_sql_clause()
+ if num_real_where_clauses == 0:
+ # handle the special case where there were no actual WHERE clauses
+ where = where.replace('WHERE GROUP BY', 'GROUP BY')
+ else:
+ where = where.replace('AND GROUP BY', 'GROUP BY')
+
+ return ('SELECT ' + ', '.join(select_fields) + where), params
+
+
+ def get_group_counts(self, query, group_by, extra_select_fields=[]):
+ """
+ Performs the given query grouped by the fields in group_by. Returns a
+ list of rows, where each row is a list containing the value of each
+ field in group_by, followed by the group count.
+ """
+ sql, params = self._get_group_query_sql(query, group_by,
+ extra_select_fields)
+ cursor = readonly_connection.connection.cursor()
+ num_rows = cursor.execute(sql, params)
+ return cursor.fetchall()
+
+
+ def _get_num_groups_sql(self, query, group_by):
+ group_fields = self._get_field_names(group_by)
+ query._order_by = None # this can mess up the query is isn't needed
+ _, where, params = query._get_sql_clause()
+ return ('SELECT COUNT(DISTINCT %s) %s' % (','.join(group_fields),
+ where),
+ params)
+
+
+ def get_num_groups(self, query, group_by):
+ """
+ Returns the number of distinct groups for the given query grouped by the
+ fields in group_by.
+ """
+ sql, params = self._get_num_groups_sql(query, group_by)
+ cursor = readonly_connection.connection.cursor()
+ cursor.execute(sql, params)
+ return cursor.fetchone()[0]
+
+
+class Machine(dbmodels.Model):
+ machine_idx = dbmodels.IntegerField(primary_key=True)
+ hostname = dbmodels.CharField(unique=True, maxlength=300)
+ machine_group = dbmodels.CharField(blank=True, maxlength=240)
+ owner = dbmodels.CharField(blank=True, maxlength=240)
+
+ class Meta:
+ db_table = 'machines'
+
+
+class Kernel(dbmodels.Model):
+ kernel_idx = dbmodels.IntegerField(primary_key=True)
+ kernel_hash = dbmodels.CharField(maxlength=105, editable=False)
+ base = dbmodels.CharField(maxlength=90)
+ printable = dbmodels.CharField(maxlength=300)
+
+ class Meta:
+ db_table = 'kernels'
+
+
+class Patch(dbmodels.Model):
+ kernel = dbmodels.ForeignKey(Kernel, db_column='kernel_idx')
+ name = dbmodels.CharField(blank=True, maxlength=240)
+ url = dbmodels.CharField(blank=True, maxlength=900)
+ hash_ = dbmodels.CharField(blank=True, maxlength=105, db_column='hash')
+
+ class Meta:
+ db_table = 'patches'
+
+
+class Status(dbmodels.Model):
+ status_idx = dbmodels.IntegerField(primary_key=True)
+ word = dbmodels.CharField(maxlength=30)
+
+ class Meta:
+ db_table = 'status'
+
+
+class Job(dbmodels.Model):
+ job_idx = dbmodels.IntegerField(primary_key=True)
+ tag = dbmodels.CharField(unique=True, maxlength=300)
+ label = dbmodels.CharField(maxlength=300)
+ username = dbmodels.CharField(maxlength=240)
+ machine = dbmodels.ForeignKey(Machine, db_column='machine_idx')
+ queued_time = dbmodels.DateTimeField(null=True, blank=True)
+ started_time = dbmodels.DateTimeField(null=True, blank=True)
+ finished_time = dbmodels.DateTimeField(null=True, blank=True)
+
+ class Meta:
+ db_table = 'jobs'
+
+
+class TestManager(dbmodels.Manager):
+ 'Custom manager for Test model'
+ def query_using_test_view(self, filter_data):
+ test_ids = [test_view.test_idx for test_view
+ in TestView.query_objects(filter_data)]
+ return self.in_bulk(test_ids).values()
+
+
+class Test(dbmodels.Model):
+ test_idx = dbmodels.IntegerField(primary_key=True)
+ job = dbmodels.ForeignKey(Job, db_column='job_idx')
+ test = dbmodels.CharField(maxlength=90)
+ subdir = dbmodels.CharField(blank=True, maxlength=180)
+ kernel = dbmodels.ForeignKey(Kernel, db_column='kernel_idx')
+ status = dbmodels.ForeignKey(Status, db_column='status')
+ reason = dbmodels.CharField(blank=True, maxlength=3072)
+ machine = dbmodels.ForeignKey(Machine, db_column='machine_idx')
+ finished_time = dbmodels.DateTimeField(null=True, blank=True)
+ started_time = dbmodels.DateTimeField(null=True, blank=True)
+
+ objects = TestManager()
+
+ class Meta:
+ db_table = 'tests'
+
+
+class TestAttribute(dbmodels.Model):
+ test = dbmodels.ForeignKey(Test, db_column='test_idx')
+ attribute = dbmodels.CharField(maxlength=90)
+ value = dbmodels.CharField(blank=True, maxlength=300)
+
+ class Meta:
+ db_table = 'test_attributes'
+
+
+class IterationAttribute(dbmodels.Model):
+ test = dbmodels.ForeignKey(Test, db_column='test_idx')
+ iteration = dbmodels.IntegerField()
+ attribute = dbmodels.CharField(maxlength=90)
+ value = dbmodels.CharField(blank=True, maxlength=300)
+
+ class Meta:
+ db_table = 'iteration_attributes'
+
+
+class IterationResult(dbmodels.Model):
+ test = dbmodels.ForeignKey(Test, db_column='test_idx')
+ iteration = dbmodels.IntegerField()
+ attribute = dbmodels.CharField(maxlength=90)
+ value = dbmodels.FloatField(null=True, max_digits=12, decimal_places=31,
+ blank=True)
+
+ class Meta:
+ db_table = 'iteration_result'
+
+
+class TestLabel(dbmodels.Model, model_logic.ModelExtensions):
+ name = dbmodels.CharField(maxlength=80)
+ description = dbmodels.TextField(blank=True)
+ tests = dbmodels.ManyToManyField(Test, blank=True,
+ filter_interface=dbmodels.HORIZONTAL)
+
+ name_field = 'name'
+
+ class Meta:
+ db_table = 'test_labels'
+
+
+class SavedQuery(dbmodels.Model, model_logic.ModelExtensions):
+ # TODO: change this to foreign key once DBs are merged
+ owner = dbmodels.CharField(maxlength=80)
+ name = dbmodels.CharField(maxlength=100)
+ url_token = dbmodels.TextField()
+
+ class Meta:
+ db_table = 'saved_queries'
+
+
+# views
+
+class TestViewManager(TempManager):
+ class _JoinQ(dbmodels.Q):
+ def __init__(self):
+ self._joins = datastructures.SortedDict()
+
+
+ def add_join(self, table, condition, join_type, alias=None):
+ if alias is None:
+ alias = table
+ self._joins[alias] = (table, join_type, condition)
+
+
+ def get_sql(self, opts):
+ return self._joins, [], []
+
+
+ def get_query_set(self):
+ query = super(TestViewManager, self).get_query_set()
+
+ # add extra fields to selects, using the SQL itself as the "alias"
+ extra_select = dict((sql, sql)
+ for sql in self.model.extra_fields.iterkeys())
+ return query.extra(select=extra_select)
+
+
+ def get_query_set_with_labels(self, filter_data):
+ query_set = self.get_query_set()
+ # TODO: make this check more thorough if necessary
+ if 'test_labels' in filter_data.get('extra_where', ''):
+ filter_object = self._JoinQ()
+ filter_object.add_join(
+ 'test_labels_tests',
+ 'test_labels_tests.test_id = test_view.test_idx',
+ 'LEFT JOIN')
+ filter_object.add_join(
+ 'test_labels',
+ 'test_labels.id = test_labels_tests.testlabel_id',
+ 'LEFT JOIN')
+ query_set = query_set.complex_filter(filter_object).distinct()
+ else:
+ filter_data['no_distinct'] = True
+ return query_set
+
+
+class TestView(dbmodels.Model, model_logic.ModelExtensions):
+ extra_fields = {
+ 'DATE(test_finished_time)' : 'test finished day',
+ }
+
+ group_fields = [
+ 'test_name',
+ 'status',
+ 'kernel',
+ 'hostname',
+ 'job_tag',
+ 'job_name',
+ 'platform',
+ 'reason',
+ 'job_owner',
+ 'test_finished_time',
+ 'DATE(test_finished_time)',
+ ]
+
+ test_idx = dbmodels.IntegerField('test index', primary_key=True)
+ job_idx = dbmodels.IntegerField('job index', null=True, blank=True)
+ test_name = dbmodels.CharField(blank=True, maxlength=90)
+ subdir = dbmodels.CharField('subdirectory', blank=True, maxlength=180)
+ kernel_idx = dbmodels.IntegerField('kernel index')
+ status_idx = dbmodels.IntegerField('status index')
+ reason = dbmodels.CharField(blank=True, maxlength=3072)
+ machine_idx = dbmodels.IntegerField('host index')
+ test_started_time = dbmodels.DateTimeField(null=True, blank=True)
+ test_finished_time = dbmodels.DateTimeField(null=True, blank=True)
+ job_tag = dbmodels.CharField(blank=True, maxlength=300)
+ job_name = dbmodels.CharField(blank=True, maxlength=300)
+ job_owner = dbmodels.CharField('owner', blank=True, maxlength=240)
+ job_queued_time = dbmodels.DateTimeField(null=True, blank=True)
+ job_started_time = dbmodels.DateTimeField(null=True, blank=True)
+ job_finished_time = dbmodels.DateTimeField(null=True, blank=True)
+ hostname = dbmodels.CharField(blank=True, maxlength=300)
+ platform = dbmodels.CharField(blank=True, maxlength=240)
+ machine_owner = dbmodels.CharField(blank=True, maxlength=240)
+ kernel_hash = dbmodels.CharField(blank=True, maxlength=105)
+ kernel_base = dbmodels.CharField(blank=True, maxlength=90)
+ kernel = dbmodels.CharField(blank=True, maxlength=300)
+ status = dbmodels.CharField(blank=True, maxlength=30)
+
+ objects = TestViewManager()
+
+ def save(self):
+ raise NotImplementedError('TestView is read-only')
+
+
+ def delete(self):
+ raise NotImplementedError('TestView is read-only')
+
+
+ @classmethod
+ def query_objects(cls, filter_data, initial_query=None):
+ if initial_query is None:
+ initial_query = cls.objects.get_query_set_with_labels(filter_data)
+ return super(TestView, cls).query_objects(filter_data,
+ initial_query=initial_query)
+
+
+ @classmethod
+ def list_objects(cls, filter_data, initial_query=None):
+ """
+ Django's ValuesQuerySet (used when you call query.values()) doesn't
+ support custom select fields, so we have to basically reimplement it
+ here.
+ TODO: merge this up to ModelExtensions after some settling time.
+ """
+ query = cls.query_objects(filter_data, initial_query=initial_query)
+ object_dicts = []
+ for model_object in query:
+ object_dict = model_object.get_object_dict()
+ for sql in cls.extra_fields.iterkeys():
+ object_dict[sql] = getattr(model_object, sql)
+ object_dicts.append(object_dict)
+ return object_dicts
+
+
+ class Meta:
+ db_table = 'test_view_2'
diff --git a/new_tko/tko/rpc_interface.py b/new_tko/tko/rpc_interface.py
new file mode 100644
index 0000000..d2b35b0
--- /dev/null
+++ b/new_tko/tko/rpc_interface.py
@@ -0,0 +1,243 @@
+import re, datetime
+from django.db import models as dbmodels
+from autotest_lib.frontend import thread_local
+from autotest_lib.frontend.afe import rpc_utils, model_logic
+from autotest_lib.new_tko.tko import models, tko_rpc_utils
+
+# table/spreadsheet view support
+
+def get_test_views(**filter_data):
+ return rpc_utils.prepare_for_serialization(
+ models.TestView.list_objects(filter_data))
+
+
+def get_num_test_views(**filter_data):
+ return models.TestView.query_count(filter_data)
+
+
+def get_group_counts(group_by, header_groups=[], extra_select_fields=[],
+ **filter_data):
+ """
+ Queries against TestView grouping by the specified fields and computings
+ counts for each group.
+ * group_by should be a list of field names.
+ * extra_select_fields can be used to specify additional fields to select
+ (usually for aggregate functions).
+ * header_groups can be used to get lists of unique combinations of group
+ fields. It should be a list of tuples of fields from group_by. It's
+ primarily for use by the spreadsheet view.
+
+ Returns a dictionary with two keys:
+ * header_values contains a list of lists, one for each header group in
+ header_groups. Each list contains all the values for the corresponding
+ header group as tuples.
+ * groups contains a list of dicts, one for each row. Each dict contains
+ keys for each of the group_by fields, plus a 'group_count' key for the
+ total count in the group, plus keys for each of the extra_select_fields.
+ The keys for the extra_select_fields are determined by the "AS" alias of
+ the field.
+ """
+ group_by = list(set(group_by)) # eliminate duplicates
+
+ query = models.TestView.query_objects(filter_data)
+ counts = models.TestView.objects.get_group_counts(query, group_by,
+ extra_select_fields)
+ if len(counts) > tko_rpc_utils.MAX_GROUP_RESULTS:
+ raise tko_rpc_utils.TooManyRowsError(
+ 'Query yielded %d rows, exceeding maximum %d' % (
+ len(counts), tko_rpc_utils.MAX_GROUP_RESULTS))
+
+ extra_field_names = [sql.lower().rsplit(' as ', 1)[1]
+ for sql in extra_select_fields]
+ group_processor = tko_rpc_utils.GroupDataProcessor(group_by, header_groups,
+ extra_field_names)
+ group_dicts = [group_processor.get_group_dict(count_row)
+ for count_row in counts]
+
+ header_values = group_processor.get_sorted_header_values()
+ if header_groups:
+ for group_dict in group_dicts:
+ group_processor.replace_headers_with_indices(group_dict)
+
+ result = {'header_values' : header_values,
+ 'groups' : group_dicts}
+ return rpc_utils.prepare_for_serialization(result)
+
+
+def get_num_groups(group_by, **filter_data):
+ """
+ Gets the count of unique groups with the given grouping fields.
+ """
+ query = models.TestView.query_objects(filter_data)
+ return models.TestView.objects.get_num_groups(query, group_by)
+
+
+# SQL expression to compute passed test count for test groups
+_PASS_COUNT_SQL = 'SUM(IF(status="GOOD", 1, 0)) AS pass_count'
+_COMPLETE_COUNT_SQL = ('SUM(IF(NOT (status="TEST_NA" OR '
+ 'status="RUNNING" OR '
+ 'status="NOSTATUS"), 1, 0)) '
+ 'AS complete_count')
+_INCOMPLETE_COUNT_SQL = ('SUM(IF(status="RUNNING", 1, 0)) '
+ 'AS incomplete_count')
+
+def get_status_counts(group_by, header_groups=[], **filter_data):
+ """
+ Like get_group_counts, but also computes counts of passed, complete (and
+ valid), and incomplete tests, stored in keys "pass_count', 'complete_count',
+ and 'incomplete_count', respectively.
+ """
+ extra_fields = [_PASS_COUNT_SQL, _COMPLETE_COUNT_SQL, _INCOMPLETE_COUNT_SQL]
+ return get_group_counts(group_by, extra_select_fields=extra_fields,
+ header_groups=header_groups, **filter_data)
+
+
+def get_test_logs_urls(**filter_data):
+ """
+ Return URLs to test logs for all tests matching the filter data.
+ """
+ query = models.TestView.query_objects(filter_data)
+ tests = set((test_view.job_tag, test_view.test) for test_view in query)
+ links = []
+ for job_tag, test in tests:
+ links.append('/results/' + job_tag + '/' + test)
+ return links
+
+
+def get_job_ids(**filter_data):
+ """
+ Returns AFE job IDs for all tests matching the filters.
+ """
+ query = models.TestView.query_objects(filter_data)
+ job_ids = set()
+ for test_view in query.values('job_tag').distinct():
+ # extract job ID from tag
+ job_ids.add(int(test_view['job_tag'].split('-')[0]))
+ return list(job_ids)
+
+
+# graphing view support
+
+def get_hosts_and_tests():
+ """\
+ Gets every host that has had a benchmark run on it. Additionally, also
+ gets a dictionary mapping the host names to the benchmarks.
+ """
+
+ host_info = {}
+ q = (dbmodels.Q(test_name__startswith='kernbench') |
+ dbmodels.Q(test_name__startswith='dbench') |
+ dbmodels.Q(test_name__startswith='tbench') |
+ dbmodels.Q(test_name__startswith='unixbench') |
+ dbmodels.Q(test_name__startswith='iozone'))
+ test_query = models.TestView.objects.filter(q).values(
+ 'test_name', 'hostname', 'machine_idx').distinct()
+ for result_dict in test_query:
+ hostname = result_dict['hostname']
+ test = result_dict['test_name']
+ machine_idx = result_dict['machine_idx']
+ host_info.setdefault(hostname, {})
+ host_info[hostname].setdefault('tests', [])
+ host_info[hostname]['tests'].append(test)
+ host_info[hostname]['id'] = machine_idx
+ return rpc_utils.prepare_for_serialization(host_info)
+
+
+# test label management
+
+def add_test_label(name, description=None):
+ return models.TestLabel.add_object(name=name, description=description).id
+
+
+def modify_test_label(label_id, **data):
+ models.TestLabel.smart_get(label_id).update_object(data)
+
+
+def delete_test_label(label_id):
+ models.TestLabel.smart_get(label_id).delete()
+
+
+def get_test_labels(**filter_data):
+ return rpc_utils.prepare_for_serialization(
+ models.TestLabel.list_objects(filter_data))
+
+
+def get_test_labels_for_tests(**test_filter_data):
+ tests = models.Test.objects.query_using_test_view(test_filter_data)
+ labels = models.TestLabel.list_objects({'tests__in' : tests})
+ return rpc_utils.prepare_for_serialization(labels)
+
+
+def test_label_add_tests(label_id, **test_filter_data):
+ print label_id
+ test_objs = models.Test.objects.query_using_test_view(test_filter_data)
+ models.TestLabel.smart_get(label_id).tests.add(*test_objs)
+
+
+def test_label_remove_tests(label_id, **test_filter_data):
+ test_objs = models.Test.objects.query_using_test_view(test_filter_data)
+ models.TestLabel.smart_get(label_id).tests.remove(*test_objs)
+
+
+# saved queries
+
+def get_saved_queries(**filter_data):
+ return rpc_utils.prepare_for_serialization(
+ models.SavedQuery.list_objects(filter_data))
+
+
+def add_saved_query(name, url_token):
+ name = name.strip()
+ owner = thread_local.get_user()
+ existing_list = list(models.SavedQuery.objects.filter(owner=owner,
+ name=name))
+ if existing_list:
+ query_object = existing_list[0]
+ query_object.url_token = url_token
+ query_object.save()
+ return query_object.id
+
+ return models.SavedQuery.add_object(owner=owner, name=name,
+ url_token=url_token).id
+
+
+def delete_saved_queries(id_list):
+ user = thread_local.get_user()
+ query = models.SavedQuery.objects.filter(id__in=id_list, owner=user)
+ if query.count() == 0:
+ raise model_logic.ValidationError('No such queries found for this user')
+ query.delete()
+
+
+# other
+
+_benchmark_key = {
+ 'kernbench' : 'elapsed',
+ 'dbench' : 'throughput',
+ 'tbench' : 'throughput',
+ 'unixbench' : 'score',
+ 'iozone' : '32768-4096-fwrite'
+}
+
+
+def get_static_data():
+ result = {}
+ group_fields = []
+ for field in models.TestView.group_fields:
+ if field in models.TestView.extra_fields:
+ name = models.TestView.extra_fields[field]
+ else:
+ name = models.TestView.get_field_dict()[field].verbose_name
+ group_fields.append((name.capitalize(), field))
+ model_fields = [(field.verbose_name.capitalize(), field.column)
+ for field in models.TestView._meta.fields]
+ extra_fields = [(field_name.capitalize(), field_sql)
+ for field_sql, field_name
+ in models.TestView.extra_fields.iteritems()]
+
+ result['group_fields'] = sorted(group_fields)
+ result['all_fields'] = sorted(model_fields + extra_fields)
+ result['test_labels'] = get_test_labels(sort_by=['name'])
+ result['user_login'] = thread_local.get_user()
+ result['benchmark_key'] = _benchmark_key
+ return result
diff --git a/new_tko/tko/tko_rpc_utils.py b/new_tko/tko/tko_rpc_utils.py
new file mode 100644
index 0000000..341bed1
--- /dev/null
+++ b/new_tko/tko/tko_rpc_utils.py
@@ -0,0 +1,115 @@
+from autotest_lib.frontend.afe import rpc_utils
+from autotest_lib.client.bin import kernel_versions
+
+MAX_GROUP_RESULTS = 50000
+
+class TooManyRowsError(Exception):
+ """
+ Raised when a database query returns too many rows.
+ """
+
+
+class KernelString(str):
+ """
+ Custom string class that uses correct kernel version comparisons.
+ """
+ def _map(self):
+ return kernel_versions.version_encode(self)
+
+
+ def __hash__(self):
+ return hash(self._map())
+
+
+ def __eq__(self, other):
+ return self._map() == other._map()
+
+
+ def __ne__(self, other):
+ return self._map() != other._map()
+
+
+ def __lt__(self, other):
+ return self._map() < other._map()
+
+
+ def __lte__(self, other):
+ return self._map() <= other._map()
+
+
+ def __gt__(self, other):
+ return self._map() > other._map()
+
+
+ def __gte__(self, other):
+ return self._map() >= other._map()
+
+
+class GroupDataProcessor(object):
+ def __init__(self, group_by, header_groups, extra_fields):
+ self._group_by = group_by
+ self._num_group_fields = len(group_by)
+ self._header_value_sets = [set() for i
+ in xrange(len(header_groups))]
+ self._header_groups = header_groups
+ self._sorted_header_values = None
+ self._extra_fields = extra_fields
+
+
+ @staticmethod
+ def _get_field(group_dict, field):
+ """
+ Wraps kernel versions with a KernelString so they sort properly.
+ """
+ value = group_dict[field]
+ if field == 'kernel':
+ return KernelString(value)
+ if value is None: # handle null dates as later than everything else
+ if field.startswith('DATE('):
+ return rpc_utils.NULL_DATE
+ if field.endswith('_time'):
+ return rpc_utils.NULL_DATETIME
+ return value
+
+
+ def get_group_dict(self, count_row):
+ group_values = count_row[:self._num_group_fields]
+ group_dict = dict(zip(self._group_by, group_values))
+ group_dict['group_count'] = count_row[self._num_group_fields]
+
+ extra_values = [int(value)
+ for value in count_row[(self._num_group_fields + 1):]]
+ group_dict.update(zip(self._extra_fields, extra_values))
+
+ # compute and aggregate header groups
+ for i, group in enumerate(self._header_groups):
+ header = tuple(self._get_field(group_dict, field)
+ for field in group)
+ self._header_value_sets[i].add(header)
+ group_dict.setdefault('header_values', []).append(header)
+
+ # frontend's SelectionManager needs a unique ID
+ group_dict['id'] = str(group_values)
+ return group_dict
+
+
+ def get_sorted_header_values(self):
+ sorted_header_values = [sorted(value_set)
+ for value_set in self._header_value_sets]
+ # construct dicts mapping headers to their indices, for use in
+ # replace_headers_with_indices()
+ self._header_index_maps = []
+ for value_list in sorted_header_values:
+ index_map = dict((value, i) for i, value in enumerate(value_list))
+ self._header_index_maps.append(index_map)
+
+ return sorted_header_values
+
+
+ def replace_headers_with_indices(self, group_dict):
+ group_dict['header_indices'] = [index_map[header_value]
+ for index_map, header_value
+ in zip(self._header_index_maps,
+ group_dict['header_values'])]
+ for field in self._group_by + ['header_values']:
+ del group_dict[field]
diff --git a/new_tko/tko/urls.py b/new_tko/tko/urls.py
new file mode 100644
index 0000000..1214cae
--- /dev/null
+++ b/new_tko/tko/urls.py
@@ -0,0 +1,27 @@
+from django.conf.urls.defaults import *
+from django.conf import settings
+import os
+
+pattern_list = [(r'^(?:|noauth/)rpc/', 'new_tko.tko.views.handle_rpc')]
+
+debug_pattern_list = [
+ (r'^model_doc/', 'new_tko.tko.views.model_documentation'),
+
+ # for GWT hosted mode
+ (r'^(?P<forward_addr>autotest.*)',
+ 'autotest_lib.frontend.afe.views.gwt_forward'),
+
+ # for GWT compiled files
+ (r'^client/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': os.path.join(os.path.dirname(__file__), '..', '..',
+ 'frontend', 'client', 'www')}),
+ # redirect / to compiled client
+ (r'^$', 'django.views.generic.simple.redirect_to',
+ {'url': 'client/autotest.TkoClient/TkoClient.html'}),
+
+]
+
+if settings.DEBUG:
+ pattern_list += debug_pattern_list
+
+urlpatterns = patterns('', *pattern_list)
diff --git a/new_tko/tko/views.py b/new_tko/tko/views.py
new file mode 100644
index 0000000..7985ef5
--- /dev/null
+++ b/new_tko/tko/views.py
@@ -0,0 +1,9 @@
+from new_tko.tko import rpc_interface
+from autotest_lib.frontend.afe import rpc_handler
+
+rpc_handler_obj = rpc_handler.RpcHandler((rpc_interface,),
+ document_module=rpc_interface)
+
+
+def handle_rpc(request):
+ return rpc_handler_obj.handle_rpc_request(request)
diff --git a/new_tko/urls.py b/new_tko/urls.py
new file mode 100644
index 0000000..b425328
--- /dev/null
+++ b/new_tko/urls.py
@@ -0,0 +1,21 @@
+from django.conf.urls.defaults import *
+from django.conf import settings
+
+RE_PREFIX = '^' + settings.URL_PREFIX
+
+pattern_list = (
+ (RE_PREFIX + r'admin/', include('django.contrib.admin.urls')),
+ (RE_PREFIX, include('new_tko.tko.urls')),
+)
+
+debug_pattern_list = (
+ # redirect /tko and /results to local apache server
+ (r'^(?P<path>(tko|results)/.*)$',
+ 'frontend.afe.views.redirect_with_extra_data',
+ {'url': 'http://%(server_name)s/%(path)s?%(getdata)s'}),
+)
+
+if settings.DEBUG:
+ pattern_list += debug_pattern_list
+
+urlpatterns = patterns('', *pattern_list)