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/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;
+ }
+ }
+}