Browse Source

HUE-641 [jb] Auto-refresh the Job Browser views

Added ajax refresh on job list and job view
Enrico Berti 12 years ago
parent
commit
87a713e

+ 2 - 0
apps/jobbrowser/src/jobbrowser/templates/attempt_logs.mako

@@ -120,6 +120,8 @@ ${ line | unicode,trim }
 
 <script type="text/javascript" charset="utf-8">
   $(document).ready(function () {
+    enableResizeLogs();
+
     $("#metadataTable").dataTable({
       "bPaginate": false,
       "bLengthChange": false,

+ 272 - 93
apps/jobbrowser/src/jobbrowser/templates/job.mako

@@ -26,8 +26,8 @@
   from django.utils.translation import ugettext as _
 %>
 
-<%def name="task_table(tasks)">
-    <table class="taskTable table table-striped table-condensed">
+<%def name="task_table(dom_id, tasks)">
+    <table id="${ dom_id }" class="taskTable table table-striped table-condensed">
         <thead>
         <tr>
             <th>${_('Logs')}</th>
@@ -92,6 +92,17 @@
 
 ${ commonheader(_('Job: %(jobId)s') % dict(jobId=job.jobId_short), "jobbrowser", user) | n,unicode }
 
+<style type="text/css">
+  .killJob {
+    display: none;
+  }
+  %if not failed_tasks:
+    #failedTasksContainer {
+      display: none;
+    }
+  %endif
+</style>
+
 <div class="container-fluid">
     <h1>${_('Job: %(jobId)s - Job Browser') % dict(jobId=job.jobId_short)}</h1>
     <div class="row-fluid">
@@ -103,25 +114,20 @@ ${ commonheader(_('Job: %(jobId)s') % dict(jobId=job.jobId_short), "jobbrowser",
                     <li class="nav-header">${_('User')}</li>
                     <li>${job.user}</li>
                     <li class="nav-header">${_('Status')}</li>
-                    <li>
-                        ${comps.get_status(job)}
-                    </li>
+                    <li id="jobStatus">&nbsp;</li>
                     <li class="nav-header">${_('Logs')}</li>
                     <li><a href="${ url('jobbrowser.views.job_single_logs', job=job.jobId) }"><i class="icon-tasks"></i> ${_('Logs')}</a></li>
-                    % if not job.is_mr2 and (job.status.lower() in ('running', 'pending')):
-                        <li class="nav-header">${_('Kill Job')}</li>
-                        <li>
-                          <a href="#" title="${_('Kill this job')}" onclick="$('#kill-job').submit()">${_('Kill this job')}</a>
-                          <form id="kill-job" action="${url('jobbrowser.views.kill_job', job=job.jobId)}?next=${request.get_full_path()|urlencode}" method="POST"></form>
-                        </li>
-                    % endif
+
                     % if not job.is_retired:
                         <li class="nav-header">${_('Maps:')}</li>
-                        <li>${comps.mr_graph_maps(job)}</li>
+                        <li id="jobMaps">&nbsp;</li>
                         <li class="nav-header">${_('Reduces:')}</li>
-                        <li>${comps.mr_graph_reduces(job)}</li>
+                        <li id="jobReduces">&nbsp;</li>
                     % endif
 
+                    <li class="nav-header">${_('Duration:')}</li>
+                    <li id="jobDuration">&nbsp;</li>
+
                     <%
                         output_dir = job.conf_keys.get('mapredOutputDir', "")
                         location_url = location_to_url(output_dir)
@@ -146,6 +152,9 @@ ${ commonheader(_('Job: %(jobId)s') % dict(jobId=job.jobId_short), "jobbrowser",
                         % endif
                         </li>
                     % endif
+
+                    <li class="nav-header killJob">${_('Actions')}</li>
+                    <li id="killJobContainer" class="killJob"></li>
                 </ul>
             </div>
         </div>
@@ -168,8 +177,8 @@ ${ commonheader(_('Job: %(jobId)s') % dict(jobId=job.jobId_short), "jobbrowser",
                     <div class="tab-pane active" id="attempts">
                         <table id="jobAttemptTable" class="table table-striped table-condensed">
                             <thead>
-                                <th>${_('Logs')}</th>
-                                <th>${_('Id')}</th>
+                                <th width="20">${_('Logs')}</th>
+                                <th width="30">${_('Id')}</th>
                                 <th>${_('Container')}</th>
                             </thead>
                             <tbody>
@@ -197,24 +206,22 @@ ${ commonheader(_('Job: %(jobId)s') % dict(jobId=job.jobId_short), "jobbrowser",
                        <br/>
                        <br/>
                     % else:
-                        %if failed_tasks:
+                        <div id="failedTasksContainer">
+                            <a style="float:right;margin-right:10px;margin-top: 10px" href="${url('jobbrowser.views.tasks', job=job.jobId)}?taskstate=failed">${_('View All Failed Tasks')} &raquo;</a>
+                            <h3>
+                                ${_('Failed Tasks')}
+                            </h3>
                             <div>
-                                <h3>
-                                    <a href="${url('jobbrowser.views.tasks', job=job.jobId)}?taskstate=failed">${_('View Failed Tasks')} &raquo;</a>
-                                    ${_('Failed Tasks')}
-                                </h3>
-                                <div>
-                                ${task_table(failed_tasks)}
-                                </div>
+                            ${task_table('failedTasks', failed_tasks)}
                             </div>
-                        %endif
+                        </div>
                         <div>
-                            <a style="float:right;margin-right:10px" href="${url('jobbrowser.views.tasks', job=job.jobId)}">${_('View All Tasks')} &raquo;</a>
+                            <a style="float:right;margin-right:10px;margin-top: 10px" href="${url('jobbrowser.views.tasks', job=job.jobId)}">${_('View All Tasks')} &raquo;</a>
                             <h3>
                                 ${_('Recent Tasks')}
                             </h3>
                             <div>
-                                ${task_table(recent_tasks)}
+                                ${task_table('recentTasks', recent_tasks)}
                             </div>
                         </div>
                     % endif
@@ -318,81 +325,253 @@ ${ commonheader(_('Job: %(jobId)s') % dict(jobId=job.jobId_short), "jobbrowser",
     </div>
 </div>
 
+
+<div id="killModal" class="modal hide fade">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${_('Please Confirm')}</h3>
+  </div>
+  <div class="modal-body">
+    <p>${_('Are you sure you want to kill this job?')}</p>
+  </div>
+  <div class="modal-footer">
+    <a class="btn" data-dismiss="modal">${_('No')}</a>
+    <a id="killJobBtn" class="btn btn-danger disable-feedback">${_('Yes')}</a>
+  </div>
+</div>
+
+<script src="/jobbrowser/static/js/utils.js" type="text/javascript" charset="utf-8"></script>
+
 <script type="text/javascript" charset="utf-8">
-    $(document).ready(function(){
-        $(".taskTable").dataTable({
-            "bPaginate": false,
-            "bLengthChange": false,
-            "bInfo": false,
-            "bAutoWidth": false,
-            "aoColumns": [
-                { "sWidth": "1%", "bSortable": false },
-                { "sWidth": "50%" },
-                { "sWidth": "49%" }
-            ],
-            "aaSorting": [[ 1, "asc" ]],
-            "oLanguage": {
-                "sEmptyTable": "${_('No data available')}",
-                "sZeroRecords": "${_('No matching records')}"
-            }
-        });
+$(document).ready(function () {
+  $(".taskTable").dataTable({
+    "bPaginate": false,
+    "bLengthChange": false,
+    "bInfo": false,
+    "bAutoWidth": false,
+    "aoColumns": [
+      { "sWidth": "1%", "bSortable": false },
+      { "sWidth": "50%" },
+      { "sWidth": "49%" }
+    ],
+    "aaSorting": [
+      [ 1, "asc" ]
+    ],
+    "oLanguage": {
+      "sEmptyTable": "${_('No data available')}",
+      "sZeroRecords": "${_('No matching records')}"
+    }
+  });
 
-        var _metadataTable = $("#metadataTable").dataTable({
-            "bPaginate": false,
-            "bLengthChange": false,
-            "bInfo": false,
-            "bAutoWidth": false,
-            "aoColumns": [
-                { "sWidth": "30%" },
-                { "sWidth": "70%" }
-            ],
-            "oLanguage": {
-                "sEmptyTable": "${_('No data available')}",
-                "sZeroRecords": "${_('No matching records')}"
-            }
-        });
+  var _metadataTable = $("#metadataTable").dataTable({
+    "bPaginate": false,
+    "bLengthChange": false,
+    "bInfo": false,
+    "bAutoWidth": false,
+    "aoColumns": [
+      { "sWidth": "30%" },
+      { "sWidth": "70%" }
+    ],
+    "oLanguage": {
+      "sEmptyTable": "${_('No data available')}",
+      "sZeroRecords": "${_('No matching records')}"
+    }
+  });
 
-        var _rawConfigurationTable = $("#rawConfigurationTable").dataTable({
-            "bPaginate": false,
-            "bLengthChange": false,
-            "bInfo": false,
-            "bAutoWidth": false,
-            "aoColumns": [
-                { "sWidth": "30%" },
-                { "sWidth": "70%" }
-            ],
-            "oLanguage": {
-                "sEmptyTable": "${_('No data available')}",
-                "sZeroRecords": "${_('No matching records')}"
-            }
-        });
+  var _rawConfigurationTable = $("#rawConfigurationTable").dataTable({
+    "bPaginate": false,
+    "bLengthChange": false,
+    "bInfo": false,
+    "bAutoWidth": false,
+    "aoColumns": [
+      { "sWidth": "30%" },
+      { "sWidth": "70%" }
+    ],
+    "oLanguage": {
+      "sEmptyTable": "${_('No data available')}",
+      "sZeroRecords": "${_('No matching records')}"
+    }
+  });
+
+  $("#metadataFilter").keyup(function () {
+    _metadataTable.fnFilter($(this).val());
+    _rawConfigurationTable.fnFilter($(this).val());
+  });
+
+  $(".jobCountersTable").dataTable({
+    "bPaginate": false,
+    "bLengthChange": false,
+    "bInfo": false,
+    "bAutoWidth": false,
+    "aoColumns": [
+      { "sWidth": "40%" },
+      { "sWidth": "20%" },
+      { "sWidth": "20%" },
+      { "sWidth": "20%" }
+    ],
+    "oLanguage": {
+      "sEmptyTable": "${_('No data available')}",
+      "sZeroRecords": "${_('No matching records')}"
+    }
+  });
+
+  $(".dataTables_wrapper").css("min-height", "0");
+  $(".dataTables_filter").hide();
+
+  $(document).ajaxError(function (event, jqxhr, settings, exception) {
+    if (jqxhr.status == 500) {
+      window.clearInterval(_runningInterval);
+      $.jHueNotify.error("${_('There was a problem communicating with the server. Please refresh the page.')}");
+    }
+  });
+
+  var _isUpdating = true;
+
+  function callJobDetails() {
+    _isUpdating = true;
+    $.getJSON("?format=json&rnd=" + Math.random(), function (data) { // Need to add random to prevent the cached of IE9
+      if (data != null && data.job != null) {
+        updateJob(data.job);
+        updateFailedTasks(data.failedTasks);
+        updateRecentTasks(data.recentTasks);
+      }
+      _isUpdating = false;
+    });
+  }
+
+  function updateJob(job) {
+    var killCell = "";
+    if (job.canKill) {
+      killCell = '<button class="btn kill" ' +
+              'href="javascript:void(0)" ' +
+              'data-url="' + job.url + '" ' +
+              'data-killurl="' + job.killUrl + '" ' +
+              'data-shortid="' + job.shortId + '" ' +
+              'title="${ _('Kill this job') }" ' +
+              '>${ _('Kill this job') }</button>';
+      $(".killJob").show();
+    }
+    else {
+      $(".killJob").hide();
+    }
+    $("#killJobContainer").html(killCell);
+    $("#jobStatus").html('<span class="label ' + getStatusClass(job.status) + '">' + (job.isRetired && !job.isMR2 ? '<i class="icon-briefcase icon-white" title="${ _('Retired') }"></i> ' : '') + job.status + '</span>');
+    if (job.desiredMaps > 0) {
+      $("#jobMaps").html('<span title="' + emptyStringIfNull(job.mapsPercentComplete) + '">' + (job.isRetired ? '${_('N/A')}' : '<div class="progress" title="' + (job.isMR2 ? job.mapsPercentComplete : job.finishedMaps + '/' + job.desiredMaps) + '"><div class="bar-label">' + job.mapsPercentComplete + '%</div><div class="' + 'bar ' + getStatusClass(job.status, "bar-") + '" style="margin-top:-20px;width:' + job.mapsPercentComplete + '%"></div></div>') + '</span>');
+    }
+    else {
+      $("#jobMaps").html('${_('N/A')}');
+    }
+    if (job.desiredReduces > 0) {
+      $("#jobReduces").html('<span title="' + emptyStringIfNull(job.reducesPercentComplete) + '">' + (job.isRetired ? '${_('N/A')}' : '<div class="progress" title="' + (job.isMR2 ? job.reducesPercentComplete : job.finishedReduces + '/' + job.desiredReduces) + '"><div class="bar-label">' + job.reducesPercentComplete + '%</div><div class="' + 'bar ' + getStatusClass(job.status, "bar-") + '" style="margin-top:-20px;width:' + job.reducesPercentComplete + '%"></div></div>') + '</span>');
+    }
+    else {
+      $("#jobReduces").html('${_('N/A')}');
+    }
+    $("#jobDuration").html('<span title="' + emptyStringIfNull(job.durationMs) + '">' + (job.isRetired ? '${_('N/A')}' : emptyStringIfNull(job.durationFormatted)) + '</span>');
+
+    if (['RUNNING', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED'].indexOf(job.status.toUpperCase()) == -1) {
+      window.clearInterval(_runningInterval);
+      removeFailedTasksFromRecent();
+    }
+  }
 
-        $("#metadataFilter").keyup(function(){
-            _metadataTable.fnFilter($(this).val());
-            _rawConfigurationTable.fnFilter($(this).val());
+  function updateFailedTasks(tasks) {
+    if (tasks != null && tasks.length > 0 && $("#failedTasks").length > 0) {
+      $("#failedTasksContainer").show();
+      var _failedTasksTableNodes = $("#failedTasks").dataTable().fnGetNodes();
+      $(tasks).each(function (cnt, task) {
+        var _foundRow = null;
+        $(_failedTasksTableNodes).each(function (iNode, node) {
+          if ($(node).children("td").eq(1).text().trim() == task.shortId) {
+            _foundRow = node;
+          }
         });
+        if (_foundRow == null) {
+          $("#failedTasks").dataTable().fnAddData(getTaskRow(task));
+        }
+      });
+    }
+  }
 
-        $(".jobCountersTable").dataTable({
-            "bPaginate": false,
-            "bLengthChange": false,
-            "bInfo": false,
-            "bAutoWidth": false,
-            "aoColumns": [
-                { "sWidth": "40%" },
-                { "sWidth": "20%" },
-                { "sWidth": "20%" },
-                { "sWidth": "20%" }
-            ],
-            "oLanguage": {
-                "sEmptyTable": "${_('No data available')}",
-                "sZeroRecords": "${_('No matching records')}"
-            }
+  function updateRecentTasks(tasks) {
+    if (tasks != null && tasks.length > 0 && $("#recentTasks").length > 0) {
+      var _recentTasksTableNodes = $("#recentTasks").dataTable().fnGetNodes();
+      $(tasks).each(function (cnt, task) {
+        var _foundRow = null;
+        $(_recentTasksTableNodes).each(function (iNode, node) {
+          if ($(node).children("td").eq(1).text().trim() == task.shortId) {
+            _foundRow = node;
+          }
         });
+        if (_foundRow == null) {
+          $("#recentTasks").dataTable().fnAddData(getTaskRow(task));
+        }
+      });
+    }
+  }
 
-        $(".dataTables_wrapper").css("min-height","0");
-        $(".dataTables_filter").hide();
-        $("a[data-row-selector='true']").jHueRowSelector();
+  function removeFailedTasksFromRecent() {
+    var _failedTasksTableNodes = $("#failedTasks").dataTable().fnGetNodes();
+    var _recentTasksTableNodes = $("#recentTasks").dataTable().fnGetNodes();
+    $(_failedTasksTableNodes).each(function (fCnt, fNode) {
+      $(_recentTasksTableNodes).each(function (rCnt, rNode) {
+        if ($(rNode).children("td").eq(1).text().trim() == $(fNode).children("td").eq(1).text().trim()) {
+          $("#recentTasks").dataTable().fnDeleteRow(rCnt);
+        }
+      });
     });
+  }
+
+  function getTaskRow(task) {
+    return [
+      '<a href="' + emptyStringIfNull(task.logs) + '" data-row-selector-exclude="true"><i class="icon-tasks"></i></a>',
+      '<a href="' + emptyStringIfNull(task.url) + '" title="${_('View this task')}" data-row-selector="true">' + emptyStringIfNull(task.shortId) + '</a>',
+      emptyStringIfNull(task.type)
+    ]
+  }
+
+  $(document).on("click", ".kill", function (e) {
+    var _this = $(this);
+    $("#killJobBtn").data("url", _this.data("url"));
+    $("#killJobBtn").data("killurl", _this.data("killurl"));
+    $("#killModal").modal({
+      keyboard: true,
+      show: true
+    });
+  });
+
+  $("#killJobBtn").on("click", function () {
+    var _this = $(this);
+    _this.attr("data-loading-text", _this.text() + " ...");
+    _this.button("loading");
+    $.post(_this.data("killurl"),
+            {
+              "format": "json"
+            },
+            function (response) {
+              _this.button("reset");
+              $("#killModal").modal("hide");
+              if (response.status != 0) {
+                $.jHueNotify.error("${ _('There was a problem killing this job.') }");
+              }
+              else {
+                callJobDetails({ url: _this.data("url")});
+              }
+            }
+    );
+  });
+
+  callJobDetails();
+
+  var _runningInterval = window.setInterval(function () {
+    if (!_isUpdating){
+      callJobDetails();
+    }
+  }, 2000);
+
+  $("a[data-row-selector='true']").jHueRowSelector();
+});
 </script>
 
 

+ 2 - 0
apps/jobbrowser/src/jobbrowser/templates/job_attempt_logs.mako

@@ -73,6 +73,8 @@ ${ commonheader(_('Job Attempt: %(attempt_index)s') % {'attempt_index': attempt_
 
 <script type="text/javascript" charset="utf-8">
   $(document).ready(function () {
+    enableResizeLogs();
+
     $("#metadataTable").dataTable({
       "bPaginate": false,
       "bLengthChange": false,

+ 319 - 174
apps/jobbrowser/src/jobbrowser/templates/jobs.mako

@@ -19,205 +19,350 @@
   from desktop.views import commonheader, commonfooter
   from django.utils.translation import ugettext as _
 %>
-<%namespace name="comps" file="jobbrowser_components.mako" />
-
-<%def name="get_state(option, state)">
-%   if option == state:
-      selected="true"
-%   endif
-</%def>
+<%namespace name="actionbar" file="actionbar.mako" />
 
 ${ commonheader(None, "jobbrowser", user) | n,unicode }
+
+<link href="/jobbrowser/static/css/jobbrowser.css" rel="stylesheet">
+
 <div class="container-fluid">
   <h1>${_('Job Browser')}</h1>
-  <form class="well form-inline" action="/jobbrowser/jobs" method="GET">
-    <label>
-    ${_('Job status:')}
-    <select name="state" class="submitter">
-        <option value="all" ${get_state('all', state_filter)}>${_('All States')}</option>
-        <option value="running" ${get_state('running', state_filter)}>${_('Running')}</option>
-        <option value="completed" ${get_state('completed', state_filter)}>${_('Completed')}</option>
-        <option value="failed" ${get_state('failed', state_filter)}>${_('Failed')}</option>
-        <option value="killed" ${get_state('killed', state_filter)}>${_('Killed')}</option>
-    </select>
-    </label>
-    &nbsp;
-    <label class="checkbox">
+
+  <%actionbar:render>
+    <%def name="search()">
+      ${_('Username')} <input id="userFilter" type="text" class="input-medium search-query" placeholder="${_('Search for username')}" value="${ user_filter or '' }">
+      &nbsp;&nbsp;${_('Text')} <input id="textFilter" type="text" class="input-xlarge search-query" placeholder="${_('Search for text')}" value="${ text_filter or '' }">
+      <img id="loading" src="/static/art/spinner.gif" />
+    </%def>
+
+    <%def name="creation()">
+      <label class="checkbox retired">
         <%
             checked = ""
             if retired == "on":
                 checked = 'checked="checked"'
         %>
-        <input name="retired" type="checkbox" class="submitter" ${checked}> ${_('Show retired jobs')}
-    </label>
-
-    <label class="pull-right">
-        &nbsp;
-        ${_('Text:')}
-        <input type="text" name="text" title="${_('Text Filter')}" value="${ text_filter or '' }" placeholder="${_('Text Filter')}" class="submitter input-large search-query"/>
-    </label>
-    <label class="pull-right">
-        ${_('Username:')}
-        <input type="text" name="user" title="${_('User Name Filter')}" value="${ user_filter or '' }" placeholder="${_('User Name Filter')}" class="submitter input-large search-query" />
-    </label>
-  </form>
-
-
-% if not jobs:
-<p>${_('There were no jobs that match your search criteria.')}</p>
-% else:
-<style>
-    .job-row {
-        height:45px;
-    }
-</style>
-<table class="datatables table table-striped table-condensed">
+        <input id="showRetired" type="checkbox" ${checked}> ${_('Show retired jobs')}
+      </label>
+      <span class="btn-group">
+        <a class="btn btn-status btn-success" data-value="completed">${ _('Succeeded') }</a>
+        <a class="btn btn-status btn-warning" data-value="running">${ _('Running') }</a>
+        <a class="btn btn-status btn-danger disable-feedback" data-value="failed">${ _('Failed') }</a>
+        <a class="btn btn-status btn-danger disable-feedback" data-value="killed">${ _('Killed') }</a>
+      </span>
+    </%def>
+  </%actionbar:render>
+
+  <div id="noJobs" class="alert"><i class="icon-warning-sign"></i>&nbsp; ${_('There are no jobs that match your search criteria.')}</div>
+
+  <table id="jobsTable" class="datatables table table-striped table-condensed">
     <thead>
-        <tr>
-            <th width="4%">${_('Logs')}</th>
-            <th width="10%">${_('ID')}</th>
-            <th width="49%">${_('Name')}</th>
-            <th width="5%">${_('Status')}</th>
-            <th width="5%">${_('User')}</th>
-            <th width="2%">${_('Maps')}</th>
-            <th width="2%">${_('Reduces')}</th>
-            <th width="5%">${_('Queue')}</th>
-            <th width="4%">${_('Priority')}</th>
-            <th width="4%">${_('Duration')}</th>
-            <th width="12%">${_('Date')}</th>
-            <th width="3%" data-row-selector-exclude="true" ></th>
-        </tr>
+    <tr>
+      <th width="4%">${_('Logs')}</th>
+      <th width="10%">${_('ID')}</th>
+      <th width="49%">${_('Name')}</th>
+      <th width="5%">${_('Status')}</th>
+      <th width="5%">${_('User')}</th>
+      <th width="2%">${_('Maps')}</th>
+      <th width="2%">${_('Reduces')}</th>
+      <th width="5%">${_('Queue')}</th>
+      <th width="4%">${_('Priority')}</th>
+      <th width="4%">${_('Duration')}</th>
+      <th width="12%">${_('Date')}</th>
+      <th width="3%" data-row-selector-exclude="true"></th>
+    </tr>
     </thead>
     <tbody>
-        % for job in jobs:
-        <tr class="job-row">
-            <td data-row-selector-exclude="true">
-                <a href="${ url('jobbrowser.views.job_single_logs', job=job.jobId) }" data-row-selector-exclude="true"><i class="icon-tasks"></i></a>
-            </td>
-            <td>
-                <a href="${url('jobbrowser.views.single_job', job=job.jobId)}" title="${_('View this job')}" data-row-selector="true">${job.jobId_short}</a>
-            </td>
-            <td>
-                ${job.jobName}
-            </td>
-            <td>
-                <a href="${url('jobbrowser.views.jobs')}?${get_state_link(request, 'state', job.status.lower())}"
-                    title="${_('Show only %(status)s jobs') % dict(status=job.status.lower())}" class="nounderline">
-                    ${comps.get_status(job)}
-                </a>
-            </td>
-            <td>
-                <a href="${url('jobbrowser.views.jobs')}?${get_state_link(request, 'user', job.user.lower())}"
-                    title="${_('Show only %(status)s jobs') % dict(status=job.user.lower())}">${job.user}</a>
-            </td>
-            <td data-sort-value="${job.maps_percent_complete}">
-                % if job.is_retired:
-                    <div class="center">${_('N/A')}</div>
-                % else:
-                    ${comps.mr_graph_maps(job)}
-                % endif
-            </td>
-                <td data-sort-value="${job.reduces_percent_complete}">
-                % if job.is_retired:
-                    <div class="center">${_('N/A')}</div>
-                % else:
-                    ${comps.mr_graph_reduces(job)}
-                % endif
-            </td>
-            <td>${job.queueName}</td>
-            <td>${job.priority.lower() or _('N/A')}</td>
-            <td data-sort-value="${job.durationInMillis}" data-row-selector-exclude="true">
-                % if job.is_retired:
-                    ${_('N/A')}
-                % else:
-                    ${job.durationFormatted}
-                % endif
-            </td>
-            <td data-sort-value="${job.startTimeMs}">${job.startTimeFormatted}</td>
-            <td data-row-selector-exclude="true" style="padding-right: 60px">
-                % if (job.status.lower() == 'running' or job.status.lower() == 'pending') and not job.is_mr2:
-                  % if request.user.is_superuser or request.user.username == job.user:
-                    <a href="#" title="${_('Kill this job')}" kill-action="${url('jobbrowser.views.kill_job', job=job.jobId)}?next=${request.get_full_path() | urlencode}"
-                        data-row-selector-exclude="true" data-keyboard="true" class="btn btn-mini kill">
-                      <i class="icon-remove"></i> ${_('Kill')}
-                    </a>
-                  % endif
-                % endif
-            </td>
-            </tr>
-            % endfor
-        </tbody>
-    </table>
-    % endif
+    </tbody>
+  </table>
 </div>
 
 <div id="killModal" class="modal hide fade">
-    <div class="modal-header">
-        <a href="#" class="close" data-dismiss="modal">&times;</a>
-        <h3>${_('Please Confirm')}</h3>
-    </div>
-    <div class="modal-body">
-        <p>${_('Are you sure you want to kill this job?')}</p>
-    </div>
-    <div class="modal-footer">
-        <form id="kill-job" action="" method="POST" class="form-stacked">
-            <a id="cancelKillBtn" class="btn">${_('No')}</a>
-            <input type="submit" value="${_('Yes')}" class="btn btn-danger" />
-        </form>
-    </div>
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${_('Please Confirm')}</h3>
+  </div>
+  <div class="modal-body">
+    <p>${_('Are you sure you want to kill this job?')}</p>
+  </div>
+  <div class="modal-footer">
+    <a class="btn" data-dismiss="modal">${_('No')}</a>
+    <a id="killJobBtn" class="btn btn-danger disable-feedback">${_('Yes')}</a>
+  </div>
 </div>
 
+<script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
+<script src="/jobbrowser/static/js/utils.js" type="text/javascript" charset="utf-8"></script>
+
 <script type="text/javascript" charset="utf-8">
-    $(document).ready(function(){
-        $(".datatables").dataTable({
-            "bPaginate": false,
-            "bLengthChange": false,
-            "bFilter": false,
-            "bInfo": false,
-            "aaSorting": [[1, "desc"]],
-            "aoColumns": [
-                {"bSortable": false},
-                null,
-                null,
-                null,
-                null,
-                { "sSortDataType": "dom-sort-value", "sType": "numeric", "sWidth": "60px" },
-                { "sSortDataType": "dom-sort-value", "sType": "numeric", "sWidth": "60px" },
-                null,
-                null,
-                { "sSortDataType": "dom-sort-value", "sType": "numeric" },
-                { "sSortDataType": "dom-sort-value", "sType": "numeric" },
-                {"bSortable":false}
-            ],
-            "oLanguage": {
-                "sEmptyTable": "${_('No data available')}",
-                "sZeroRecords": "${_('No matching records')}",
+
+  $(document).ready(function () {
+
+    var jobTable = $(".datatables").dataTable({
+      "sPaginationType": "bootstrap",
+      "iDisplayLength": 30,
+      "bLengthChange": false,
+      "sDom": "<'row'r>t<'row'<'span6'i><''p>>",
+      "aaSorting": [
+        [1, "desc"]
+      ],
+      "aoColumns": [
+        {"bSortable": false, "sWidth": "20px"},
+        null,
+        null,
+        null,
+        null,
+        { "sType": "title-numeric", "sWidth": "60px" },
+        { "sType": "title-numeric", "sWidth": "60px" },
+        null,
+        null,
+        { "sType": "title-numeric" },
+        { "sType": "title-numeric" },
+        {"bSortable": false}
+      ],
+      "oLanguage": {
+        "sEmptyTable": "${_('No data available')}",
+        "sZeroRecords": "${_('No matching records')}"
+      }
+    });
+
+    $(document).ajaxError(function (event, jqxhr, settings, exception) {
+      if (jqxhr.status == 500) {
+        window.clearInterval(_runningInterval);
+        $.jHueNotify.error("${_('There was a problem communicating with the server. Please refresh the page.')}");
+      }
+    });
+
+    function populateTable(data) {
+      if (data != null) {
+        jobTable.fnClearTable();
+        $("#loading").addClass("hide");
+        $("#noJobs").hide();
+        $(".datatables").show();
+        if (data.length == 0) {
+          $("#noJobs").show();
+          $(".datatables").hide();
+        }
+        else {
+          $(data).each(function (cnt, job) {
+            try {
+              jobTable.fnAddData(getJobRow(job));
+              $("a[data-row-selector='true']").jHueRowSelector();
             }
-        });
+            catch (error) {
+              $.jHueNotify.error(error);
+            }
+          });
+        }
+      }
+    }
 
-        $(document).on("click", ".kill", function(e){
-            $("#kill-job").attr("action", $(e.target).attr("kill-action"));
-            $("#killModal").modal({
-                keyboard: true,
-                show: true
-            });
-        });
+    var _isUpdating = false;
+    var newRows = [];
 
-        $("#cancelKillBtn").click(function(){
-            $("#killModal").modal("hide");
+    function updateRunning(data) {
+      if (data != null && data.length > 0) {
+        for (var i = 0; i < newRows.length; i++) {
+          var isDone = true;
+          $(data).each(function (cnt, job) {
+            if (job.id == newRows[i].id) {
+              isDone = false;
+            }
+          });
+          if (isDone) {
+            callJobDetails(newRows[i]);
+            newRows.splice(i);
+          }
+        }
+        $(data).each(function (cnt, job) {
+          var nNodes = jobTable.fnGetNodes();
+          var foundRow = null;
+          $(nNodes).each(function (iNode, node) {
+            if ($(node).children("td").eq(1).text().trim() == job.shortId) {
+              foundRow = node;
+            }
+          });
+          if (foundRow == null) {
+            if (['RUNNING', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED'].indexOf(job.status.toUpperCase()) > -1) {
+              newRows.push(job);
+              try {
+                jobTable.fnAddData(getJobRow(job));
+                $("a[data-row-selector='true']").jHueRowSelector();
+              }
+              catch (error) {
+                $.jHueNotify.error(error);
+              }
+            }
+          }
+          else {
+            updateJobRow(job, foundRow);
+          }
         });
+      }
+      else {
+        for (var i = 0; i < newRows.length; i++) {
+          callJobDetails(newRows[i]);
+          newRows.splice(i);
+        }
+      }
+      _isUpdating = false;
+    }
 
-        var filterTimeout = -1;
-        $(".search-query").keyup(function(){
-            window.clearTimeout(filterTimeout);
-            var el = $(this);
-            filterTimeout = window.setTimeout(function(){
-                el.closest("form").submit();
-            }, 500);
-        });
+    function getJobRow(job) {
+      var _killCell = "";
+      if (job.canKill) {
+        _killCell = '<a class="btn btn-small btn-inverse kill" ' +
+                'href="javascript:void(0)" ' +
+                'data-url="' + job.url + '" ' +
+                'data-killUrl="' + job.killUrl + '" ' +
+                'data-shortid="' + job.shortId + '" ' +
+                'title="${ _('Kill this job') }" ' +
+                '>${ _('Kill') }</a>';
+      }
+      return [
+        '<a href="' + emptyStringIfNull(job.logs) + '" data-row-selector-exclude="true"><i class="icon-tasks"></i></a>',
+        '<a href="' + emptyStringIfNull(job.url) + '" title="${_('View this job')}" data-row-selector="true">' + emptyStringIfNull(job.shortId) + '</a>',
+        emptyStringIfNull(job.name),
+        '<span class="label ' + getStatusClass(job.status) + '">' + (job.isRetired && !job.isMR2 ? '<i class="icon-briefcase icon-white" title="${ _('Retired') }"></i> ' : '') + job.status + '</span>',
+        emptyStringIfNull(job.user),
+        '<span title="' + emptyStringIfNull(job.mapsPercentComplete) + '">' + (job.isRetired ? '${_('N/A')}' : '<div class="progress" title="' + (job.isMR2 ? job.mapsPercentComplete : job.finishedMaps + '/' + job.desiredMaps) + '"><div class="bar-label">' + job.mapsPercentComplete + '%</div><div class="' + 'bar ' + getStatusClass(job.status, "bar-") + '" style="margin-top:-20px;width:' + job.mapsPercentComplete + '%"></div></div>') + '</span>',
+        '<span title="' + emptyStringIfNull(job.reducesPercentComplete) + '">' + (job.isRetired ? '${_('N/A')}' : '<div class="progress" title="' + (job.isMR2 ? job.reducesPercentComplete : job.finishedReduces + '/' + job.desiredReduces) + '"><div class="bar-label">' + job.reducesPercentComplete + '%</div><div class="' + 'bar ' + getStatusClass(job.status, "bar-") + '" style="margin-top:-20px;width:' + job.reducesPercentComplete + '%"></div></div>') + '</span>',
+        emptyStringIfNull(job.queueName),
+        emptyStringIfNull(job.priority),
+        '<span title="' + emptyStringIfNull(job.durationMs) + '">' + (job.isRetired ? '${_('N/A')}' : emptyStringIfNull(job.durationFormatted)) + '</span>',
+        '<span title="' + emptyStringIfNull(job.startTimeMs) + '">' + emptyStringIfNull(job.startTimeFormatted) + '</span>',
+        _killCell
+      ]
+    }
+
+    function updateJobRow(job, row) {
+      jobTable.fnUpdate('<span class="label ' + getStatusClass(job.status) + '">' + job.status + '</span>', row, 3, false);
+      jobTable.fnUpdate('<span title="' + emptyStringIfNull(job.mapsPercentComplete) + '"><div class="progress" title="' + (job.isMR2 ? job.mapsPercentComplete : job.finishedMaps + '/' + job.desiredMaps) + '"><div class="bar-label">' + job.mapsPercentComplete + '%</div><div class="' + 'bar ' + getStatusClass(job.status, "bar-") + '" style="margin-top:-20px;width:' + job.mapsPercentComplete + '%"></div></div></span>', row, 5, false);
+      jobTable.fnUpdate('<span title="' + emptyStringIfNull(job.reducesPercentComplete) + '"><div class="progress" title="' + (job.isMR2 ? job.reducesPercentComplete : job.finishedReduces + '/' + job.desiredReduces) + '"><div class="bar-label">' + job.reducesPercentComplete + '%</div><div class="' + 'bar ' + getStatusClass(job.status, "bar-") + '" style="margin-top:-20px;width:' + job.reducesPercentComplete + '%"></div></div></span>', row, 6, false);
+      jobTable.fnUpdate('<span title="' + emptyStringIfNull(job.durationMs) + '">' + emptyStringIfNull(job.durationFormatted) + '</span>', row, 9, false);
+      var _killCell = "";
+      if (job.canKill) {
+        _killCell = '<a class="btn btn-small btn-inverse kill" ' +
+                'href="javascript:void(0)" ' +
+                'data-url="' + job.url + '" ' +
+                'data-killurl="' + job.killUrl + '" ' +
+                'data-shortid="' + job.shortId + '" ' +
+                'title="${ _('Kill this job') }" ' +
+                '>${ _('Kill') }</a>';
+      }
+      jobTable.fnUpdate(_killCell, row, 11, false);
+    }
+
+    function callJobDetails(job) {
+      $.getJSON(job.url + "?format=json&rnd=" + Math.random(), function (job) {
+        if (job != null) {
+          var jobTableNodes = jobTable.fnGetNodes();
+          var _foundRow = null;
+          $(jobTableNodes).each(function (iNode, node) {
+            if ($(node).children("td").eq(1).text().trim() == job.shortId) {
+              _foundRow = node;
+            }
+          });
+          if (_foundRow != null) {
+            updateJobRow(job, _foundRow);
+          }
+        }
+      });
+    }
+
+    function callJsonData(callback, justRunning) {
+      var _url = "?format=json";
+      if (justRunning == undefined) {
+        if ($(".btn-status.active").length > 0) {
+          _url += "&state=" + $(".btn-status.active").data("value");
+        }
+        else {
+          _url += "&state=all";
+        }
+      }
+      else {
+        _isUpdating = true;
+        _url += "&state=running";
+      }
+      _url += "&user=" + $("#userFilter").val().trim();
+      if ($("#textFilter").val().trim() != "") {
+        _url += "&text=" + $("#textFilter").val().trim();
+      }
+      if ($("#showRetired").is(":checked")) {
+        _url += "&retired=on";
+      }
+      _url += "&rnd=" + Math.random(); // thanks IE!
+      $.getJSON(_url, callback);
+    }
+
+    var _filterTimeout = -1;
+    $(".search-query").on("keyup", function () {
+      window.clearTimeout(_filterTimeout);
+      _filterTimeout = window.setTimeout(function () {
+        $("#loading").removeClass("hide");
+        callJsonData(populateTable);
+      }, 300);
+    });
 
-        $("a[data-row-selector='true']").jHueRowSelector();
+    $("#showRetired").change(function () {
+      $("#loading").removeClass("hide");
+      callJsonData(populateTable);
     });
+
+    $(".btn-status").on("click", function () {
+      $(".btn-status").not($(this)).removeClass("active");
+      $(this).toggleClass("active");
+      $("#loading").removeClass("hide");
+      callJsonData(populateTable);
+    });
+
+
+    $(document).on("click", ".kill", function (e) {
+      var _this = $(this);
+      $("#killJobBtn").data("url", _this.data("url"));
+      $("#killJobBtn").data("killurl", _this.data("killurl"));
+      $("#killModal").modal({
+        keyboard: true,
+        show: true
+      });
+    });
+
+    $("#killJobBtn").on("click", function () {
+      var _this = $(this);
+      _this.attr("data-loading-text", _this.text() + " ...");
+      _this.button("loading");
+      $.post(_this.data("killurl"),
+              {
+                "format": "json"
+              },
+              function (response) {
+                _this.button("reset");
+                $("#killModal").modal("hide");
+                if (response.status != 0) {
+                  $.jHueNotify.error("${ _('There was a problem killing this job.') }");
+                }
+                else {
+                  callJobDetails({ url: _this.data("url")});
+                }
+              }
+      );
+    });
+
+    // init job list
+    var _initialState = getQueryStringParameter("state");
+    if (_initialState != "") {
+      $(".btn-status[data-value='" + _initialState + "']").addClass("active");
+    }
+    callJsonData(populateTable);
+
+    var _runningInterval = window.setInterval(function () {
+      if (!_isUpdating) {
+        callJsonData(updateRunning, true);
+      }
+    }, 2000);
+
+    $("a[data-row-selector='true']").jHueRowSelector();
+  });
 </script>
 
 ${ commonfooter(messages) | n,unicode }

+ 68 - 5
apps/jobbrowser/src/jobbrowser/views.py

@@ -30,10 +30,11 @@ except ImportError:
 from django.http import HttpResponseRedirect, HttpResponse
 from django.utils.functional import wraps
 from django.utils.translation import ugettext as _
+from django.core.urlresolvers import reverse
 
 from desktop.log.access import access_warn, access_log_level
 from desktop.lib.rest.http_client import RestException
-from desktop.lib.django_util import render_json, render, copy_query_dict
+from desktop.lib.django_util import render_json, render, copy_query_dict, encode_json_for_js
 from desktop.lib.exceptions import MessageException
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.views import register_status_bar_view
@@ -69,10 +70,12 @@ def jobs(request):
   text = request.GET.get('text')
   retired = request.GET.get('retired')
 
-  jobs = get_api(request.user, request.jt).get_jobs(user=request.user, username=user, state=state, text=text, retired=retired)
+  if request.GET.get('format') == 'json':
+    jobs = get_api(request.user, request.jt).get_jobs(user=request.user, username=user, state=state, text=text, retired=retired)
+    json_jobs  = [massage_job_for_json(job, request) for job in jobs]
+    return HttpResponse(encode_json_for_js(json_jobs), mimetype="application/json")
 
   return render('jobs.mako', request, {
-    'jobs': jobs,
     'request': request,
     'state_filter': state,
     'user_filter': user,
@@ -81,6 +84,54 @@ def jobs(request):
     'filtered': not (state == 'all' and user == '' and text == '')
   })
 
+def massage_job_for_json(job, request):
+  job = {
+    'id': job.jobId,
+    'shortId': job.jobId_short,
+    'name': hasattr(job, 'jobName') and job.jobName or '',
+    'status': job.status,
+    'url': job.jobId and reverse('jobbrowser.views.single_job', kwargs={'job': job.jobId}) or '',
+    'logs': job.jobId and reverse('jobbrowser.views.job_single_logs', kwargs={'job': job.jobId}) or '',
+    'queueName': hasattr(job, 'queueName') and job.queueName or _('N/A'),
+    'priority': hasattr(job, 'priority') and job.priority.lower() or _('N/A'),
+    'user': job.user,
+    'isRetired': job.is_retired,
+    'isMR2': job.is_mr2,
+    'mapProgress': hasattr(job, 'mapProgress') and job.mapProgress or '',
+    'reduceProgress': hasattr(job, 'reduceProgress') and job.reduceProgress or '',
+    'setupProgress': hasattr(job, 'setupProgress') and job.setupProgress or '',
+    'cleanupProgress': hasattr(job, 'cleanupProgress') and job.cleanupProgress or '',
+    'desiredMaps': job.desiredMaps,
+    'desiredReduces': job.desiredReduces,
+    'mapsPercentComplete': job.maps_percent_complete,
+    'finishedMaps': job.finishedMaps,
+    'finishedReduces': job.finishedReduces,
+    'reducesPercentComplete': job.reduces_percent_complete,
+    'jobFile': hasattr(job, 'jobFile') and job.jobFile or '',
+    'launchTimeMs': hasattr(job, 'launchTimeMs') and job.launchTimeMs or '',
+    'launchTimeFormatted': hasattr(job, 'launchTimeFormatted') and job.launchTimeFormatted or '',
+    'startTimeMs': hasattr(job, 'startTimeMs') and job.startTimeMs or '',
+    'startTimeFormatted': hasattr(job, 'startTimeFormatted') and job.startTimeFormatted or '',
+    'finishTimeMs': hasattr(job, 'finishTimeMs') and job.finishTimeMs or '',
+    'finishTimeFormatted': hasattr(job, 'finishTimeFormatted') and job.finishTimeFormatted or '',
+    'durationFormatted': hasattr(job, 'durationFormatted') and job.durationFormatted or '',
+    'durationMs': hasattr(job, 'durationInMillis') and job.durationInMillis or '',
+    'canKill': (job.status.lower() == 'running' or job.status.lower() == 'pending') and not job.is_mr2 and (request.user.is_superuser or request.user.username == job.user),
+    'killUrl': job.jobId and reverse('jobbrowser.views.kill_job', kwargs={'job': job.jobId}) or ''
+  }
+  return job
+
+
+def massage_task_for_json(task):
+  task = {
+    'id': task.taskId,
+    'shortId': task.taskId_short,
+    'url': task.taskId and reverse('jobbrowser.views.single_task', kwargs={'job': task.jobId, 'taskid': task.taskId}) or '',
+    'logs': task.taskAttemptIds and reverse('jobbrowser.views.single_task_attempt_logs', kwargs={'job': task.jobId, 'taskid': task.taskId, 'attemptid': task.taskAttemptIds[-1]}) or '',
+    'type': task.taskType
+  }
+  return task
+
 
 @check_job_permission
 def single_job(request, job):
@@ -92,11 +143,21 @@ def single_job(request, job):
   recent_tasks = job.filter_tasks(task_states=('running', 'succeeded',))
   recent_tasks.sort(cmp_exec_time, reverse=True)
 
+  if request.REQUEST.get('format') == 'json':
+    json_failed_tasks = [massage_task_for_json(task) for task in failed_tasks]
+    json_recent_tasks = [massage_task_for_json(task) for task in recent_tasks]
+    json_job = {
+      'job': massage_job_for_json(job, request),
+      'failedTasks': json_failed_tasks,
+      'recentTasks': json_recent_tasks
+    }
+    return HttpResponse(encode_json_for_js(json_job), mimetype="application/json")
+
   return render('job.mako', request, {
     'request': request,
     'job': job,
-    'failed_tasks': failed_tasks[:5],
-    'recent_tasks': recent_tasks[:5],
+    'failed_tasks': failed_tasks and failed_tasks[:5] or [],
+    'recent_tasks': recent_tasks and recent_tasks[:5] or [],
   })
 
 
@@ -124,6 +185,8 @@ def kill_job(request, job):
     if job.status not in ["RUNNING", "QUEUED"]:
       if request.REQUEST.get("next"):
         return HttpResponseRedirect(request.REQUEST.get("next"))
+      elif request.REQUEST.get("format") == "json":
+        return HttpResponse(encode_json_for_js({'status': 0}), mimetype="application/json")
       else:
         raise MessageException("Job Killed")
     time.sleep(1)

+ 22 - 0
apps/jobbrowser/static/css/jobbrowser.css

@@ -0,0 +1,22 @@
+.retired {
+  display: inline;
+  margin-right: 10px;
+  margin-top: 4px;
+}
+
+.retired input {
+  float: none !important;
+  margin-right: 3px;
+  margin-top: -4px;
+}
+
+#jobsTable tbody tr {
+  height: 45px;
+}
+
+#noJobs {
+  margin-top: 40px;
+  margin-bottom: 40px;
+  padding: 20px;
+  display: none;
+}

+ 80 - 15
apps/jobbrowser/static/js/utils.js

@@ -17,7 +17,7 @@
 function initLogsElement(element) {
   element.data("logsAtEnd", true);
   element.scroll(function () {
-    element("logsAtEnd", ($(this).scrollTop() + $(this).height() + 20 >= $(this)[0].scrollHeight));
+    element.data("logsAtEnd", ($(this).scrollTop() + $(this).height() + 20 >= $(this)[0].scrollHeight));
   });
   element.css("overflow", "auto").height($(window).height() - element.offset().top - 50);
 }
@@ -34,18 +34,83 @@ function resizeLogs(element) {
   element.css("overflow", "auto").height($(window).height() - element.offset().top - 50);
 }
 
-var resizeTimeout = -1;
-var winWidth = $(window).width();
-var winHeight = $(window).height();
-
-$(window).on("resize", function () {
-  window.clearTimeout(resizeTimeout);
-  resizeTimeout = window.setTimeout(function () {
-    // prevents endless loop in IE8
-    if (winWidth != $(window).width() || winHeight != $(window).height()) {
-      $(document).trigger("resized");
-      winWidth = $(window).width();
-      winHeight = $(window).height();
+var _resizeTimeout = -1;
+var _winWidth = $(window).width();
+var _winHeight = $(window).height();
+
+function enableResizeLogs() {
+  $(window).on("resize", function () {
+    window.clearTimeout(_resizeTimeout);
+    _resizeTimeout = window.setTimeout(function () {
+      // prevents endless loop in IE8
+      if (_winWidth != $(window).width() || _winHeight != $(window).height()) {
+        $(document).trigger("resized");
+        _winWidth = $(window).width();
+        _winHeight = $(window).height();
+      }
+    }, 200);
+  });
+}
+
+function getQueryStringParameter(name) {
+  name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
+  var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
+    results = regex.exec(location.search);
+  return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
+}
+
+function getStatusClass(status, prefix) {
+  if (!Array.prototype.indexOf) {
+    Array.prototype.indexOf = function (needle) {
+      for (var i = 0; i < this.length; i++) {
+        if (this[i] === needle) {
+          return i;
+        }
+      }
+      return -1;
+    };
+  }
+  if (prefix == null) {
+    prefix = "label-";
+  }
+  var klass = "";
+  if (['SUCCEEDED', 'OK', 'DONE'].indexOf(status) > -1) {
+    klass = prefix + "success";
+  }
+  else if (['RUNNING', 'READY', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED',
+    'SUBMITTED',
+    'SUSPENDEDWITHERROR',
+    'PAUSEDWITHERROR'].indexOf(status) > -1) {
+    klass = prefix + "warning";
+  }
+  else {
+    klass = prefix + "important";
+    if (prefix == "bar-") {
+      klass = prefix + "danger";
     }
-  }, 200);
-});
+  }
+  return klass;
+}
+
+function emptyStringIfNull(obj) {
+  if (obj != null && obj != undefined) {
+    return obj;
+  }
+  return "";
+}
+
+jQuery.fn.dataTableExt.oSort['title-numeric-asc'] = function (a, b) {
+  var x = a.match(/title="*(-?[0-9\.]+)/)[1];
+  var y = b.match(/title="*(-?[0-9\.]+)/)[1];
+  x = parseFloat(x);
+  y = parseFloat(y);
+  return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+};
+
+jQuery.fn.dataTableExt.oSort['title-numeric-desc'] = function (a, b) {
+  var x = a.match(/title="*(-?[0-9\.]+)/)[1];
+  var y = b.match(/title="*(-?[0-9\.]+)/)[1];
+  x = parseFloat(x);
+  y = parseFloat(y);
+  return ((x < y) ? 1 : ((x > y) ? -1 : 0));
+};