• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package autotest.afe;
2 
3 import autotest.common.JsonRpcCallback;
4 import autotest.common.SimpleCallback;
5 import autotest.common.StaticDataRepository;
6 import autotest.common.Utils;
7 import autotest.common.table.DataTable;
8 import autotest.common.table.DataTable.DataTableListener;
9 import autotest.common.table.DynamicTable;
10 import autotest.common.table.ListFilter;
11 import autotest.common.table.RpcDataSource;
12 import autotest.common.table.SearchFilter;
13 import autotest.common.table.SelectionManager;
14 import autotest.common.table.SimpleFilter;
15 import autotest.common.table.TableDecorator;
16 import autotest.common.table.DataTable.TableWidgetFactory;
17 import autotest.common.table.DynamicTable.DynamicTableListener;
18 import autotest.common.ui.ContextMenu;
19 import autotest.common.ui.DetailView;
20 import autotest.common.ui.NotifyManager;
21 import autotest.common.ui.TableActionsPanel.TableActionsListener;
22 
23 import com.google.gwt.dom.client.Element;
24 import com.google.gwt.event.dom.client.ClickEvent;
25 import com.google.gwt.event.dom.client.ClickHandler;
26 import com.google.gwt.json.client.JSONArray;
27 import com.google.gwt.json.client.JSONBoolean;
28 import com.google.gwt.json.client.JSONNumber;
29 import com.google.gwt.json.client.JSONObject;
30 import com.google.gwt.json.client.JSONString;
31 import com.google.gwt.json.client.JSONValue;
32 import com.google.gwt.user.client.Command;
33 import com.google.gwt.user.client.ui.Button;
34 import com.google.gwt.user.client.ui.DisclosurePanel;
35 import com.google.gwt.user.client.ui.Frame;
36 import com.google.gwt.user.client.ui.HTML;
37 import com.google.gwt.user.client.ui.Label;
38 import com.google.gwt.user.client.ui.Widget;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Set;
43 
44 public class JobDetailView extends DetailView implements TableWidgetFactory {
45     private static final String[][] JOB_HOSTS_COLUMNS = {
46         {DataTable.CLICKABLE_WIDGET_COLUMN, ""}, // selection checkbox
47         {"hostname", "Host"}, {"full_status", "Status"},
48         {"host_status", "Host Status"}, {"host_locked", "Host Locked"},
49         // columns for status log and debug log links
50         {DataTable.CLICKABLE_WIDGET_COLUMN, ""}, {DataTable.CLICKABLE_WIDGET_COLUMN, ""}
51     };
52     private static final String[][] CHILD_JOBS_COLUMNS = {
53         { "id", "ID" }, { "name", "Name" }, { "priority", "Priority" },
54         { "control_type", "Client/Server" }, { JobTable.HOSTS_SUMMARY, "Status" },
55         { JobTable.RESULTS_SUMMARY, "Passed Tests" }
56     };
57     private static final String[][] JOB_HISTORY_COLUMNS = {
58         { "id", "ID" }, { "hostname", "Host" }, { "name", "Name" },
59         { "start_time", "Start Time" }, { "end_time", "End Time" },
60         { "time_used", "Time Used (seconds)" }, { "status", "Status" }
61     };
62     public static final String NO_URL = "about:blank";
63     public static final int NO_JOB_ID = -1;
64     public static final int HOSTS_PER_PAGE = 30;
65     public static final int CHILD_JOBS_PER_PAGE = 30;
66     public static final String RESULTS_MAX_WIDTH = "700px";
67     public static final String RESULTS_MAX_HEIGHT = "500px";
68 
69     public interface JobDetailListener {
onHostSelected(String hostId)70         public void onHostSelected(String hostId);
onCloneJob(JSONValue result)71         public void onCloneJob(JSONValue result);
onCreateRecurringJob(int id)72         public void onCreateRecurringJob(int id);
73     }
74 
75     protected class ChildJobsListener {
onJobSelected(int id)76         public void onJobSelected(int id) {
77             fetchById(Integer.toString(id));
78         }
79     }
80 
81     protected class JobHistoryListener {
onJobSelected(String url)82         public void onJobSelected(String url) {
83             Utils.openUrlInNewWindow(url);
84         }
85     }
86 
87     protected int jobId = NO_JOB_ID;
88 
89     private JobStatusDataSource jobStatusDataSource = new JobStatusDataSource();
90     protected JobTable childJobsTable = new JobTable(CHILD_JOBS_COLUMNS);
91     protected TableDecorator childJobsTableDecorator = new TableDecorator(childJobsTable);
92     protected SimpleFilter parentJobIdFliter = new SimpleFilter();
93     protected DynamicTable hostsTable = new DynamicTable(JOB_HOSTS_COLUMNS, jobStatusDataSource);
94     protected TableDecorator hostsTableDecorator = new TableDecorator(hostsTable);
95     protected SimpleFilter jobFilter = new SimpleFilter();
96     protected Button abortButton = new Button("Abort job");
97     protected Button cloneButton = new Button("Clone job");
98     protected Button recurringButton = new Button("Create recurring job");
99     protected Frame tkoResultsFrame = new Frame();
100 
101     protected JobDetailListener listener;
102     protected ChildJobsListener childJobsListener = new ChildJobsListener();
103     private SelectionManager hostsSelectionManager;
104     private SelectionManager childJobsSelectionManager;
105 
106     private Label controlFile = new Label();
107     private DisclosurePanel controlFilePanel = new DisclosurePanel("");
108 
109     protected StaticDataRepository staticData = StaticDataRepository.getRepository();
110 
111     protected Button getJobHistoryButton = new Button("Get Job History");
112     protected JobHistoryListener jobHistoryListener = new JobHistoryListener();
113     protected DataTable jobHistoryTable = new DataTable(JOB_HISTORY_COLUMNS);
114 
JobDetailView(JobDetailListener listener)115     public JobDetailView(JobDetailListener listener) {
116         this.listener = listener;
117         setupSpreadsheetListener(Utils.getBaseUrl());
118     }
119 
setupSpreadsheetListener(String baseUrl)120     private native void setupSpreadsheetListener(String baseUrl) /*-{
121         var ins = this;
122         $wnd.onSpreadsheetLoad = function(event) {
123             if (event.origin !== baseUrl) {
124                 return;
125             }
126             ins.@autotest.afe.JobDetailView::resizeResultsFrame(Ljava/lang/String;)(event.data);
127         }
128 
129         $wnd.addEventListener("message", $wnd.onSpreadsheetLoad, false);
130     }-*/;
131 
132     @SuppressWarnings("unused") // called from native
resizeResultsFrame(String message)133     private void resizeResultsFrame(String message) {
134         String[] parts = message.split(" ");
135         tkoResultsFrame.setSize(parts[0], parts[1]);
136     }
137 
138     @Override
fetchData()139     protected void fetchData() {
140         pointToResults(NO_URL, NO_URL, NO_URL, NO_URL, NO_URL);
141         JSONObject params = new JSONObject();
142         params.put("id", new JSONNumber(jobId));
143         rpcProxy.rpcCall("get_jobs_summary", params, new JsonRpcCallback() {
144             @Override
145             public void onSuccess(JSONValue result) {
146                 JSONObject jobObject;
147                 try {
148                     jobObject = Utils.getSingleObjectFromArray(result.isArray());
149                 }
150                 catch (IllegalArgumentException exc) {
151                     NotifyManager.getInstance().showError("No such job found");
152                     resetPage();
153                     return;
154                 }
155                 String name = Utils.jsonToString(jobObject.get("name"));
156                 String runVerify = Utils.jsonToString(jobObject.get("run_verify"));
157 
158                 showText(name, "view_label");
159                 showField(jobObject, "owner", "view_owner");
160                 String parent_job_url = Utils.jsonToString(jobObject.get("parent_job")).trim();
161                 if (parent_job_url.equals("<null>")){
162                     parent_job_url = "http://www.youtube.com/watch?v=oHg5SJYRHA0";
163                 } else {
164                     parent_job_url = "#tab_id=view_job&object_id=" + parent_job_url;
165                 }
166                 showField(jobObject, "parent_job", "view_parent");
167                 getElementById("view_parent").setAttribute("href", parent_job_url);
168                 showField(jobObject, "test_retry", "view_test_retry");
169                 double priorityValue = jobObject.get("priority").isNumber().getValue();
170                 String priorityName = staticData.getPriorityName(priorityValue);
171                 showText(priorityName, "view_priority");
172                 showField(jobObject, "created_on", "view_created");
173                 showField(jobObject, "timeout_mins", "view_timeout");
174                 String imageUrlString = "";
175                 if (jobObject.containsKey("image")) {
176                     imageUrlString = Utils.jsonToString(jobObject.get("image")).trim();
177                 }
178                 showText(imageUrlString, "view_image_url");
179                 showField(jobObject, "max_runtime_mins", "view_max_runtime");
180                 showField(jobObject, "email_list", "view_email_list");
181                 showText(runVerify, "view_run_verify");
182                 showField(jobObject, "reboot_before", "view_reboot_before");
183                 showField(jobObject, "reboot_after", "view_reboot_after");
184                 showField(jobObject, "parse_failed_repair", "view_parse_failed_repair");
185                 showField(jobObject, "synch_count", "view_synch_count");
186                 if (jobObject.get("require_ssp").isNull() != null)
187                     showText("false", "view_require_ssp");
188                 else {
189                     String require_ssp = Utils.jsonToString(jobObject.get("require_ssp"));
190                     showText(require_ssp, "view_require_ssp");
191                 }
192                 showField(jobObject, "dependencies", "view_dependencies");
193 
194                 if (staticData.getData("drone_sets_enabled").isBoolean().booleanValue()) {
195                     showField(jobObject, "drone_set", "view_drone_set");
196                 }
197 
198                 String header = Utils.jsonToString(jobObject.get("control_type")) + " control file";
199                 controlFilePanel.getHeaderTextAccessor().setText(header);
200                 controlFile.setText(Utils.jsonToString(jobObject.get("control_file")));
201 
202                 JSONObject counts = jobObject.get("status_counts").isObject();
203                 String countString = AfeUtils.formatStatusCounts(counts, ", ");
204                 showText(countString, "view_status");
205                 abortButton.setVisible(isAnyEntryAbortable(counts));
206 
207                 String shard_url = Utils.jsonToString(jobObject.get("shard")).trim();
208                 String job_id = Utils.jsonToString(jobObject.get("id")).trim();
209                 if (shard_url.equals("<null>")){
210                     shard_url = "";
211                 } else {
212                     shard_url = "http://" + shard_url;
213                 }
214                 shard_url = shard_url + "/afe/#tab_id=view_job&object_id=" + job_id;
215                 showField(jobObject, "shard", "view_job_on_shard");
216                 getElementById("view_job_on_shard").setAttribute("href", shard_url);
217                 getElementById("view_job_on_shard").setInnerHTML(shard_url);
218 
219                 String jobTag = AfeUtils.getJobTag(jobObject);
220                 pointToResults(getResultsURL(jobId), getLogsURL(jobTag),
221                                getOldResultsUrl(jobId), getTriageUrl(jobId),
222                                getEmbeddedUrl(jobId));
223 
224                 String jobTitle = "Job: " + name + " (" + jobTag + ")";
225                 displayObjectData(jobTitle);
226 
227                 jobFilter.setParameter("job", new JSONNumber(jobId));
228                 hostsTable.refresh();
229 
230                 parentJobIdFliter.setParameter("parent_job", new JSONNumber(jobId));
231                 childJobsTable.refresh();
232 
233                 jobHistoryTable.clear();
234             }
235 
236 
237             @Override
238             public void onError(JSONObject errorObject) {
239                 super.onError(errorObject);
240                 resetPage();
241             }
242         });
243     }
244 
isAnyEntryAbortable(JSONObject statusCounts)245     protected boolean isAnyEntryAbortable(JSONObject statusCounts) {
246         Set<String> statuses = statusCounts.keySet();
247         for (String status : statuses) {
248             if (!(status.equals("Completed") ||
249                   status.equals("Failed") ||
250                   status.equals("Stopped") ||
251                   status.startsWith("Aborted"))) {
252                 return true;
253             }
254         }
255         return false;
256     }
257 
258     @Override
initialize()259     public void initialize() {
260         super.initialize();
261 
262         idInput.setVisibleLength(5);
263 
264         childJobsTable.setRowsPerPage(CHILD_JOBS_PER_PAGE);
265         childJobsTable.setClickable(true);
266         childJobsTable.addListener(new DynamicTableListener() {
267             public void onRowClicked(int rowIndex, JSONObject row, boolean isRightClick) {
268                 int jobId = (int) row.get("id").isNumber().doubleValue();
269                 childJobsListener.onJobSelected(jobId);
270             }
271 
272             public void onTableRefreshed() {}
273         });
274 
275         childJobsTableDecorator.addPaginators();
276         childJobsSelectionManager = childJobsTableDecorator.addSelectionManager(false);
277         childJobsTable.setWidgetFactory(childJobsSelectionManager);
278         addWidget(childJobsTableDecorator, "child_jobs_table");
279 
280         hostsTable.setRowsPerPage(HOSTS_PER_PAGE);
281         hostsTable.setClickable(true);
282         hostsTable.addListener(new DynamicTableListener() {
283             public void onRowClicked(int rowIndex, JSONObject row, boolean isRightClick) {
284                 JSONObject host = row.get("host").isObject();
285                 String id = host.get("id").toString();
286                 listener.onHostSelected(id);
287             }
288 
289             public void onTableRefreshed() {}
290         });
291         hostsTable.setWidgetFactory(this);
292 
293         hostsTableDecorator.addPaginators();
294         addTableFilters();
295         hostsSelectionManager = hostsTableDecorator.addSelectionManager(false);
296         hostsTableDecorator.addTableActionsPanel(new TableActionsListener() {
297             public ContextMenu getActionMenu() {
298                 ContextMenu menu = new ContextMenu();
299 
300                 menu.addItem("Abort hosts", new Command() {
301                     public void execute() {
302                         abortSelectedHosts();
303                     }
304                 });
305 
306                 menu.addItem("Clone job on selected hosts", new Command() {
307                     public void execute() {
308                         cloneJobOnSelectedHosts();
309                     }
310                 });
311 
312                 if (hostsSelectionManager.isEmpty())
313                     menu.setEnabled(false);
314                 return menu;
315             }
316         }, true);
317         addWidget(hostsTableDecorator, "job_hosts_table");
318 
319         abortButton.addClickHandler(new ClickHandler() {
320             public void onClick(ClickEvent event) {
321                 abortJob();
322             }
323         });
324         addWidget(abortButton, "view_abort");
325 
326         cloneButton.addClickHandler(new ClickHandler() {
327             public void onClick(ClickEvent event) {
328                 cloneJob();
329             }
330         });
331         addWidget(cloneButton, "view_clone");
332 
333         recurringButton.addClickHandler(new ClickHandler() {
334             public void onClick(ClickEvent event) {
335                 createRecurringJob();
336             }
337         });
338         addWidget(recurringButton, "view_recurring");
339 
340         tkoResultsFrame.getElement().setAttribute("scrolling", "no");
341         addWidget(tkoResultsFrame, "tko_results");
342 
343         controlFile.addStyleName("code");
344         controlFilePanel.setContent(controlFile);
345         addWidget(controlFilePanel, "view_control_file");
346 
347         if (!staticData.getData("drone_sets_enabled").isBoolean().booleanValue()) {
348             AfeUtils.removeElement("view_drone_set_wrapper");
349         }
350 
351         getJobHistoryButton.addClickHandler(new ClickHandler() {
352             public void onClick(ClickEvent event) {
353                 getJobHistory();
354             }
355         });
356         addWidget(getJobHistoryButton, "view_get_job_history");
357 
358         jobHistoryTable.setClickable(true);
359         jobHistoryTable.addListener(new DataTableListener() {
360             public void onRowClicked(int rowIndex, JSONObject row, boolean isRightClick) {
361                 String log_url= row.get("log_url").isString().stringValue();
362                 jobHistoryListener.onJobSelected(log_url);
363             }
364 
365             public void onTableRefreshed() {}
366         });
367         addWidget(jobHistoryTable, "job_history_table");
368     }
369 
addTableFilters()370     protected void addTableFilters() {
371         hostsTable.addFilter(jobFilter);
372         childJobsTable.addFilter(parentJobIdFliter);
373 
374         SearchFilter hostnameFilter = new SearchFilter("host__hostname", true);
375         ListFilter statusFilter = new ListFilter("status");
376         StaticDataRepository staticData = StaticDataRepository.getRepository();
377         JSONArray statuses = staticData.getData("job_statuses").isArray();
378         statusFilter.setChoices(Utils.JSONtoStrings(statuses));
379 
380         hostsTableDecorator.addFilter("Hostname", hostnameFilter);
381         hostsTableDecorator.addFilter("Status", statusFilter);
382     }
383 
abortJob()384     private void abortJob() {
385         JSONObject params = new JSONObject();
386         params.put("job__id", new JSONNumber(jobId));
387         AfeUtils.callAbort(params, new SimpleCallback() {
388             public void doCallback(Object source) {
389                 refresh();
390             }
391         });
392     }
393 
abortSelectedHosts()394     private void abortSelectedHosts() {
395         AfeUtils.abortHostQueueEntries(hostsSelectionManager.getSelectedObjects(),
396                                        new SimpleCallback() {
397             public void doCallback(Object source) {
398                 refresh();
399             }
400         });
401     }
402 
cloneJob()403     protected void cloneJob() {
404         ContextMenu menu = new ContextMenu();
405         menu.addItem("Reuse any similar hosts  (default)", new Command() {
406             public void execute() {
407                 cloneJob(false);
408             }
409         });
410         menu.addItem("Reuse same specific hosts", new Command() {
411             public void execute() {
412                 cloneJob(true);
413             }
414         });
415         menu.addItem("Use failed and aborted hosts", new Command() {
416             public void execute() {
417                 JSONObject queueEntryFilterData = new JSONObject();
418                 String sql = "(status = 'Failed' OR aborted = TRUE OR " +
419                              "(host_id IS NULL AND meta_host IS NULL))";
420 
421                 queueEntryFilterData.put("extra_where", new JSONString(sql));
422                 cloneJob(true, queueEntryFilterData);
423             }
424         });
425 
426         menu.showAt(cloneButton.getAbsoluteLeft(),
427                 cloneButton.getAbsoluteTop() + cloneButton.getOffsetHeight());
428     }
429 
cloneJobOnSelectedHosts()430     private void cloneJobOnSelectedHosts() {
431         Set<JSONObject> hostsQueueEntries = hostsSelectionManager.getSelectedObjects();
432         JSONArray queueEntryIds = new JSONArray();
433         for (JSONObject queueEntry : hostsQueueEntries) {
434           queueEntryIds.set(queueEntryIds.size(), queueEntry.get("id"));
435         }
436 
437         JSONObject queueEntryFilterData = new JSONObject();
438         queueEntryFilterData.put("id__in", queueEntryIds);
439         cloneJob(true, queueEntryFilterData);
440     }
441 
cloneJob(boolean preserveMetahosts)442     private void cloneJob(boolean preserveMetahosts) {
443         cloneJob(preserveMetahosts, new JSONObject());
444     }
445 
cloneJob(boolean preserveMetahosts, JSONObject queueEntryFilterData)446     private void cloneJob(boolean preserveMetahosts, JSONObject queueEntryFilterData) {
447         JSONObject params = new JSONObject();
448         params.put("id", new JSONNumber(jobId));
449         params.put("preserve_metahosts", JSONBoolean.getInstance(preserveMetahosts));
450         params.put("queue_entry_filter_data", queueEntryFilterData);
451 
452         rpcProxy.rpcCall("get_info_for_clone", params, new JsonRpcCallback() {
453             @Override
454             public void onSuccess(JSONValue result) {
455                 listener.onCloneJob(result);
456             }
457         });
458     }
459 
createRecurringJob()460     private void createRecurringJob() {
461         listener.onCreateRecurringJob(jobId);
462     }
463 
getResultsURL(int jobId)464     private String getResultsURL(int jobId) {
465         return "/new_tko/#tab_id=spreadsheet_view&row=hostname&column=test_name&" +
466                "condition=afe_job_id+%253d+" + Integer.toString(jobId) + "&" +
467                "show_incomplete=true";
468     }
469 
getOldResultsUrl(int jobId)470     private String getOldResultsUrl(int jobId) {
471         return "/tko/compose_query.cgi?" +
472                "columns=test&rows=hostname&condition=tag%7E%27" +
473                Integer.toString(jobId) + "-%25%27&title=Report";
474     }
475 
getTriageUrl(int jobId)476     private String getTriageUrl(int jobId) {
477         /*
478          * Having a hard-coded path like this is very unfortunate, but there's no simple way
479          * in the current design to generate this link by code.
480          *
481          * TODO: Redesign the system so that we can generate these links by code.
482          *
483          * Idea: Be able to instantiate a TableView object, ask it to set up to triage this job ID,
484          *       and then ask it for the history URL.
485          */
486 
487         return "/new_tko/#tab_id=table_view&columns=test_name%252Cstatus%252Cgroup_count%252C" +
488                "reason&sort=test_name%252Cstatus%252Creason&condition=afe_job_id+%253D+" + jobId +
489                "+AND+status+%253C%253E+%2527GOOD%2527&show_invalid=false";
490     }
491 
getEmbeddedUrl(int jobId)492     private String getEmbeddedUrl(int jobId) {
493         return "/embedded_spreadsheet/EmbeddedSpreadsheetClient.html?afe_job_id=" + jobId;
494     }
495 
496     /**
497      * Get the path for a job's raw result files.
498      * @param jobLogsId id-owner, e.g. "172-showard"
499      */
getLogsURL(String jobLogsId)500     protected String getLogsURL(String jobLogsId) {
501         return Utils.getRetrieveLogsUrl(jobLogsId);
502     }
503 
pointToResults(String resultsUrl, String logsUrl, String oldResultsUrl, String triageUrl, String embeddedUrl)504     protected void pointToResults(String resultsUrl, String logsUrl,
505                                   String oldResultsUrl, String triageUrl,
506                                   String embeddedUrl) {
507         getElementById("results_link").setAttribute("href", resultsUrl);
508         getElementById("old_results_link").setAttribute("href", oldResultsUrl);
509         getElementById("raw_results_link").setAttribute("href", logsUrl);
510         getElementById("triage_failures_link").setAttribute("href", triageUrl);
511 
512         tkoResultsFrame.setSize(RESULTS_MAX_WIDTH, RESULTS_MAX_HEIGHT);
513         if (!resultsUrl.equals(NO_URL)) {
514             updateResultsFrame(tkoResultsFrame.getElement(), embeddedUrl);
515         }
516     }
517 
updateResultsFrame(Element frame, String embeddedUrl)518     private native void updateResultsFrame(Element frame, String embeddedUrl) /*-{
519         // Use location.replace() here so that the frame's URL changes don't show up in the browser
520         // window's history
521         frame.contentWindow.location.replace(embeddedUrl);
522     }-*/;
523 
524     @Override
getNoObjectText()525     protected String getNoObjectText() {
526         return "No job selected";
527     }
528 
529     @Override
getFetchControlsElementId()530     protected String getFetchControlsElementId() {
531         return "job_id_fetch_controls";
532     }
533 
534     @Override
getDataElementId()535     protected String getDataElementId() {
536         return "view_data";
537     }
538 
539     @Override
getTitleElementId()540     protected String getTitleElementId() {
541         return "view_title";
542     }
543 
544     @Override
getObjectId()545     protected String getObjectId() {
546         if (jobId == NO_JOB_ID) {
547             return NO_OBJECT;
548         }
549         return Integer.toString(jobId);
550     }
551 
552     @Override
getElementId()553     public String getElementId() {
554         return "view_job";
555     }
556 
557     @Override
setObjectId(String id)558     protected void setObjectId(String id) {
559         int newJobId;
560         try {
561             newJobId = Integer.parseInt(id);
562         }
563         catch (NumberFormatException exc) {
564             throw new IllegalArgumentException();
565         }
566         this.jobId = newJobId;
567     }
568 
createWidget(int row, int cell, JSONObject hostQueueEntry)569     public Widget createWidget(int row, int cell, JSONObject hostQueueEntry) {
570         if (cell == 0) {
571             return hostsSelectionManager.createWidget(row, cell, hostQueueEntry);
572         }
573 
574         String executionSubdir = Utils.jsonToString(hostQueueEntry.get("execution_subdir"));
575         if (executionSubdir.equals("")) {
576             // when executionSubdir == "", it's a job that hasn't yet run.
577             return null;
578         }
579 
580         JSONObject jobObject = hostQueueEntry.get("job").isObject();
581         String owner = Utils.jsonToString(jobObject.get("owner"));
582         String basePath = jobId + "-" + owner + "/" + executionSubdir + "/";
583 
584         if (cell == JOB_HOSTS_COLUMNS.length - 1) {
585             return new HTML(getLogsLinkHtml(basePath + "debug", "Debug logs"));
586         } else {
587             return new HTML(getLogsLinkHtml(basePath + "status.log", "Status log"));
588         }
589     }
590 
getLogsLinkHtml(String url, String text)591     private String getLogsLinkHtml(String url, String text) {
592         url = Utils.getRetrieveLogsUrl(url);
593         return "<a target=\"_blank\" href=\"" + url + "\">" + text + "</a>";
594     }
595 
getJobHistory()596     private void getJobHistory() {
597         JSONObject params = new JSONObject();
598         params.put("job_id", new JSONNumber(jobId));
599         AfeUtils.callGetJobHistory(params, new SimpleCallback() {
600             public void doCallback(Object result) {
601                 jobHistoryTable.clear();
602                 List<JSONObject> rows = new ArrayList<JSONObject>();
603                 JSONArray history = (JSONArray)result;
604                 for (int i = 0; i < history.size(); i++)
605                     rows.add((JSONObject)history.get(i));
606                 jobHistoryTable.addRows(rows);
607             }
608         }, true);
609     }
610 }
611