Home | History | Annotate | Download | only in afe
      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 {
     70         public void onHostSelected(String hostId);
     71         public void onCloneJob(JSONValue result);
     72         public void onCreateRecurringJob(int id);
     73     }
     74 
     75     protected class ChildJobsListener {
     76         public void onJobSelected(int id) {
     77             fetchById(Integer.toString(id));
     78         }
     79     }
     80 
     81     protected class JobHistoryListener {
     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 
    115     public JobDetailView(JobDetailListener listener) {
    116         this.listener = listener;
    117         setupSpreadsheetListener(Utils.getBaseUrl());
    118     }
    119 
    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. (at) 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
    133     private void resizeResultsFrame(String message) {
    134         String[] parts = message.split(" ");
    135         tkoResultsFrame.setSize(parts[0], parts[1]);
    136     }
    137 
    138     @Override
    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 
    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
    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 
    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 
    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 
    394     private void abortSelectedHosts() {
    395         AfeUtils.abortHostQueueEntries(hostsSelectionManager.getSelectedObjects(),
    396                                        new SimpleCallback() {
    397             public void doCallback(Object source) {
    398                 refresh();
    399             }
    400         });
    401     }
    402 
    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 
    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 
    442     private void cloneJob(boolean preserveMetahosts) {
    443         cloneJob(preserveMetahosts, new JSONObject());
    444     }
    445 
    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 
    460     private void createRecurringJob() {
    461         listener.onCreateRecurringJob(jobId);
    462     }
    463 
    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 
    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 
    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 
    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      */
    500     protected String getLogsURL(String jobLogsId) {
    501         return Utils.getRetrieveLogsUrl(jobLogsId);
    502     }
    503 
    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 
    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
    525     protected String getNoObjectText() {
    526         return "No job selected";
    527     }
    528 
    529     @Override
    530     protected String getFetchControlsElementId() {
    531         return "job_id_fetch_controls";
    532     }
    533 
    534     @Override
    535     protected String getDataElementId() {
    536         return "view_data";
    537     }
    538 
    539     @Override
    540     protected String getTitleElementId() {
    541         return "view_title";
    542     }
    543 
    544     @Override
    545     protected String getObjectId() {
    546         if (jobId == NO_JOB_ID) {
    547             return NO_OBJECT;
    548         }
    549         return Integer.toString(jobId);
    550     }
    551 
    552     @Override
    553     public String getElementId() {
    554         return "view_job";
    555     }
    556 
    557     @Override
    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 
    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 
    591     private String getLogsLinkHtml(String url, String text) {
    592         url = Utils.getRetrieveLogsUrl(url);
    593         return "<a target=\"_blank\" href=\"" + url + "\">" + text + "</a>";
    594     }
    595 
    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