Browse Source

HUE-1000 [oozie] Ajax refresh of the running workflows and coordinators in the dashboard

The dashboard now refreshes via Ajax
Added the possibility to disable the default feedback on .btn-primary and .btn-danger
Enrico Berti 13 years ago
parent
commit
871acc61d7

+ 1 - 29
apps/oozie/src/oozie/templates/dashboard/list_oozie_coordinator.mako

@@ -277,6 +277,7 @@ ${ layout.menubar(section='dashboard') }
   </div>
   </div>
 </div>
 </div>
 
 
+<script src="/oozie/static/js/utils.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/codemirror-3.0.js"></script>
 <script src="/static/ext/js/codemirror-3.0.js"></script>
 <link rel="stylesheet" href="/static/ext/css/codemirror.css">
 <link rel="stylesheet" href="/static/ext/css/codemirror.css">
@@ -292,35 +293,6 @@ ${ layout.menubar(section='dashboard') }
 </style>
 </style>
 
 
 <script>
 <script>
-  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'].indexOf(status) > -1){
-      klass = prefix + "success";
-    }
-    else if (['READY', 'RUNNING', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED', 'SUBMITTED'].indexOf(status) > -1){
-      klass = prefix + "warning";
-    }
-    else {
-      klass = prefix + "important";
-      if (prefix == "bar-"){
-        klass = prefix + "danger";
-      }
-    }
-    return klass;
-  }
 
 
   var Action = function (action) {
   var Action = function (action) {
     return {
     return {

+ 197 - 141
apps/oozie/src/oozie/templates/dashboard/list_oozie_coordinators.mako

@@ -47,8 +47,9 @@ ${layout.menubar(section='dashboard')}
         <span class="btn-group" style="float:left;">
         <span class="btn-group" style="float:left;">
           <a class="btn btn-status btn-success" data-value='SUCCEEDED'>${ _('Succeeded') }</a>
           <a class="btn btn-status btn-success" data-value='SUCCEEDED'>${ _('Succeeded') }</a>
           <a class="btn btn-status btn-warning" data-value='RUNNING'>${ _('Running') }</a>
           <a class="btn btn-status btn-warning" data-value='RUNNING'>${ _('Running') }</a>
-          <a class="btn btn-status btn-danger" data-value='KILLED'>${ _('Killed') }</a>
+          <a class="btn btn-status btn-danger disable-feedback" data-value='KILLED'>${ _('Killed') }</a>
         </span>
         </span>
+      </span>
    </form>
    </form>
   </div>
   </div>
 
 
@@ -67,33 +68,7 @@ ${layout.menubar(section='dashboard')}
         </tr>
         </tr>
       </thead>
       </thead>
       <tbody>
       <tbody>
-        % for job in jobs['running_jobs']:
-          <tr>
-            <td>${ utils.format_date(job.endTime) }</td>
-            <td>
-              <span class="label ${ utils.get_status(job.status) }">
-                ${ job.status }
-              </span>
-            </td>
-            <td>${ job.appName }</td>
-            <td data-sort-value="${ job.get_progress() }">${ job.get_progress() }%</td>
-            <td>${ job.user }</td>
-            <td><a href="${ job.get_absolute_url() }" data-row-selector="true"></a>${ job.id }</td>
-            <td>
-              % if has_job_edition_permission(job, user):
-                <a title="${_('Kill %(coordinator)s') % dict(coordinator=job.id)}"
-                  class="btn small confirmationModal"
-                  alt="${ _('Are you sure you want to kill coordinator %s?') % job.id }"
-                  href="javascript:void(0)"
-                  data-message="${ _('The coordinator was killed!') }"
-                  data-confirmation-message="${ _('Are you sure you\'d like to kill this job?') }"
-                  data-url="${ url('oozie:manage_oozie_jobs', job_id=job.id, action='kill') }">
-                    ${ _('Kill') }
-                </a>
-              % endif
-           </td>
-          </tr>
-        %endfor
+
       </tbody>
       </tbody>
     </table>
     </table>
   </div>
   </div>
@@ -112,28 +87,7 @@ ${layout.menubar(section='dashboard')}
         </tr>
         </tr>
       </thead>
       </thead>
       <tbody>
       <tbody>
-        %for job in jobs['completed_jobs']:
-          <tr>
-            <td>${ utils.format_date(job.endTime) }</td>
-            <td>
-              <span class="label
-               % if job.status == 'SUCCEEDED':
-                 label-success
-               % elif job.is_running():
-                  label-warning
-               % else:
-                 label-important
-               % endif
-               ">
-                ${job.status}
-              </span>
-            </td>
-            <td>${ job.appName }</td>
-            <td data-sort-value="${ utils.job_duration(job) }">${ utils.format_job_duration(job) }</td>
-            <td>${job.user}</td>
-            <td><a href="${ job.get_absolute_url() }" data-row-selector="true"></a>${ job.id }</td>
-          </tr>
-        %endfor
+
       </tbody>
       </tbody>
      </table>
      </table>
    </div>
    </div>
@@ -151,107 +105,128 @@ ${layout.menubar(section='dashboard')}
   </div>
   </div>
 </div>
 </div>
 
 
-
+<script src="/oozie/static/js/utils.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
 
 
 <script type="text/javascript" charset="utf-8">
 <script type="text/javascript" charset="utf-8">
-  $(document).ready(function() {
-    var runningTable = $('#running-table').dataTable( {
-      'sPaginationType': 'bootstrap',
-      'iDisplayLength': 50,
-      "bLengthChange": false,
-      "sDom": "<'row'r>t<'row'<'span6'i><''p>>",
-      "aoColumns": [
-        { "sType": "date" },
+
+  var Coordinator = function (c) {
+    return {
+      id:c.id,
+      endTime:c.endTime,
+      status:c.status,
+      statusClass:"label " + getStatusClass(c.status),
+      isRunning:c.isRunning,
+      duration:c.duration,
+      appName:decodeURIComponent(c.appName),
+      progress:c.progress,
+      progressClass:"bar " + getStatusClass(c.status, "bar-"),
+      user:c.user,
+      absoluteUrl:c.absoluteUrl,
+      canEdit:c.canEdit,
+      killUrl:c.killUrl
+    }
+  }
+
+  $(document).ready(function () {
+    var runningTable = $('#running-table').dataTable({
+      'sPaginationType':'bootstrap',
+      'iDisplayLength':50,
+      "bLengthChange":false,
+      "sDom":"<'row'r>t<'row'<'span6'i><''p>>",
+      "aoColumns":[
+        { "sType":"date" },
         null,
         null,
         null,
         null,
-        { "sSortDataType": "dom-sort-value", "sType": "numeric" },
+        { "sSortDataType":"dom-sort-value", "sType":"numeric" },
         null,
         null,
         null,
         null,
-        { "bSortable": false }
+        { "bSortable":false }
       ],
       ],
-      "aaSorting": [[ 5, "desc" ]],
-      "oLanguage": {
-            "sEmptyTable":     "${_('No data available')}",
-            "sInfo":           "${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
-            "sInfoEmpty":      "${_('Showing 0 to 0 of 0 entries')}",
-            "sInfoFiltered":   "${_('(filtered from _MAX_ total entries)')}",
-            "sZeroRecords":    "${_('No matching records')}",
-            "oPaginate": {
-                "sFirst":    "${_('First')}",
-                "sLast":     "${_('Last')}",
-                "sNext":     "${_('Next')}",
-                "sPrevious": "${_('Previous')}"
-            }
+      "aaSorting":[
+        [ 5, "desc" ]
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sInfo":"${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
+        "sInfoEmpty":"${_('Showing 0 to 0 of 0 entries')}",
+        "sInfoFiltered":"${_('(filtered from _MAX_ total entries)')}",
+        "sZeroRecords":"${_('No matching records')}",
+        "oPaginate":{
+          "sFirst":"${_('First')}",
+          "sLast":"${_('Last')}",
+          "sNext":"${_('Next')}",
+          "sPrevious":"${_('Previous')}"
+        }
       }
       }
     });
     });
 
 
-    var completedTable = $('#completed-table').dataTable( {
-      'sPaginationType': 'bootstrap',
-      'iDisplayLength': 50,
-      "bLengthChange": false,
-      "sDom": "<'row'r>t<'row'<'span6'i><''p>>",
-      "aoColumns": [
-        { "sType": "date" },
+    var completedTable = $('#completed-table').dataTable({
+      'sPaginationType':'bootstrap',
+      'iDisplayLength':50,
+      "bLengthChange":false,
+      "sDom":"<'row'r>t<'row'<'span6'i><''p>>",
+      "aoColumns":[
+        { "sType":"date" },
         null,
         null,
         null,
         null,
-        { "sSortDataType": "dom-sort-value", "sType": "numeric" },
+        { "sSortDataType":"dom-sort-value", "sType":"numeric" },
         null,
         null,
         null
         null
       ],
       ],
-      "aaSorting": [[ 5, "desc" ]],
-      "oLanguage": {
-            "sEmptyTable":     "${_('No data available')}",
-            "sInfo":           "${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
-            "sInfoEmpty":      "${_('Showing 0 to 0 of 0 entries')}",
-            "sInfoFiltered":   "${_('(filtered from _MAX_ total entries)')}",
-            "sZeroRecords":    "${_('No matching records')}",
-            "oPaginate": {
-                "sFirst":    "${_('First')}",
-                "sLast":     "${_('Last')}",
-                "sNext":     "${_('Next')}",
-                "sPrevious": "${_('Previous')}"
-            }
+      "aaSorting":[
+        [ 5, "desc" ]
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sInfo":"${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
+        "sInfoEmpty":"${_('Showing 0 to 0 of 0 entries')}",
+        "sInfoFiltered":"${_('(filtered from _MAX_ total entries)')}",
+        "sZeroRecords":"${_('No matching records')}",
+        "oPaginate":{
+          "sFirst":"${_('First')}",
+          "sLast":"${_('Last')}",
+          "sNext":"${_('Next')}",
+          "sPrevious":"${_('Previous')}"
+        }
       }
       }
     });
     });
 
 
-    $('#filterInput').keydown(function(e) {
+    $('#filterInput').keydown(function (e) {
       if (e.which == 13) {
       if (e.which == 13) {
         e.preventDefault();
         e.preventDefault();
         return false;
         return false;
       }
       }
     });
     });
 
 
-    $("#filterInput").keyup(function() {
+    $("#filterInput").keyup(function () {
       runningTable.fnFilter($(this).val());
       runningTable.fnFilter($(this).val());
       completedTable.fnFilter($(this).val());
       completedTable.fnFilter($(this).val());
 
 
-      hash = "#";
-
+      var hash = "#";
       if ($("a.btn-date.active").length > 0) {
       if ($("a.btn-date.active").length > 0) {
         hash += "date=" + $("a.btn-date.active").text();
         hash += "date=" + $("a.btn-date.active").text();
       }
       }
-
       window.location.hash = hash;
       window.location.hash = hash;
     });
     });
 
 
 
 
-    $("a.btn-status").click(function() {
+    $("a.btn-status").click(function () {
       $(this).toggleClass('active');
       $(this).toggleClass('active');
       drawTable();
       drawTable();
     });
     });
 
 
-    $("a.btn-date").click(function() {
+    $("a.btn-date").click(function () {
       $("a.btn-date").not(this).removeClass('active');
       $("a.btn-date").not(this).removeClass('active');
       $(this).toggleClass('active');
       $(this).toggleClass('active');
       drawTable();
       drawTable();
     });
     });
 
 
-    function drawTable(){
+    function drawTable() {
       runningTable.fnDraw();
       runningTable.fnDraw();
       completedTable.fnDraw();
       completedTable.fnDraw();
 
 
-      hash = "#";
+      var hash = "#";
       if ($("a.btn-date.active").length > 0) {
       if ($("a.btn-date.active").length > 0) {
         hash += "date=" + $("a.btn-date.active").text();
         hash += "date=" + $("a.btn-date.active").text();
       }
       }
@@ -259,55 +234,136 @@ ${layout.menubar(section='dashboard')}
     }
     }
 
 
     $.fn.dataTableExt.afnFiltering.push(
     $.fn.dataTableExt.afnFiltering.push(
-      function(oSettings, aData, iDataIndex) {
-        urlHashes = ""
-
-        statusBtn = $('a.btn-status.active');
-        statusFilter = true;
-        if (statusBtn.length > 0) {
-          statuses = []
-          $.each(statusBtn, function() {
-            statuses.push($(this).attr('data-value'));
-          });
-          statusFilter = aData[1].match(RegExp(statuses.join('|'), "i")) != null;
-        }
-
-        dateBtn = $('a.btn-date.active');
-        dateFilter = true;
-        if (dateBtn.length > 0) {
-          minAge = new Date() - parseInt(dateBtn.attr('data-value')) * 1000 * 60 * 60 * 24;
-          dateFilter = Date.parse(aData[0]) >= minAge;
-        }
-
-        return statusFilter && dateFilter;
-      }
+            function (oSettings, aData, iDataIndex) {
+              var urlHashes = ""
+
+              var statusBtn = $('a.btn-status.active');
+              var statusFilter = true;
+              if (statusBtn.length > 0) {
+                var statuses = []
+                $.each(statusBtn, function () {
+                  statuses.push($(this).attr('data-value'));
+                });
+                statusFilter = aData[1].match(RegExp(statuses.join('|'), "i")) != null;
+              }
+
+              var dateBtn = $('a.btn-date.active');
+              var dateFilter = true;
+              if (dateBtn.length > 0) {
+                var minAge = new Date() - parseInt(dateBtn.attr('data-value')) * 1000 * 60 * 60 * 24;
+                dateFilter = Date.parse(aData[0]) >= minAge;
+              }
+
+              return statusFilter && dateFilter;
+            }
     );
     );
 
 
     $("a[data-row-selector='true']").jHueRowSelector();
     $("a[data-row-selector='true']").jHueRowSelector();
 
 
-    $(".confirmationModal").click(function(){
+    $(document).on("click", ".confirmationModal", function () {
       var _this = $(this);
       var _this = $(this);
+      _this.bind('confirmation', function () {
+        var _this = this;
+        $.post($(this).attr("data-url"),
+                { 'notification':$(this).attr("data-message") },
+                function (response) {
+                  if (response['status'] != 0) {
+                    $.jHueNotify.error("${ _('Problem: ') }" + response['data']);
+                  } else {
+                    window.location.reload();
+                  }
+                }
+        );
+        return false;
+      });
       $("#confirmation .message").text(_this.attr("data-confirmation-message"));
       $("#confirmation .message").text(_this.attr("data-confirmation-message"));
       $("#confirmation").modal("show");
       $("#confirmation").modal("show");
-      $("#confirmation a.btn-danger").click(function() {
+      $("#confirmation a.btn-danger").click(function () {
         _this.trigger('confirmation');
         _this.trigger('confirmation');
       });
       });
     });
     });
 
 
-    $(".confirmationModal").bind('confirmation', function() {
-      var _this = this;
-      $.post($(this).attr("data-url"),
-        { 'notification': $(this).attr("data-message") },
-        function(response) {
-          if (response['status'] != 0) {
-            $.jHueNotify.error("${ _('Problem: ') }" + response['data']);
-          } else {
-            window.location.reload();
-          }
+    refreshRunning();
+    refreshCompleted();
+
+    var numRunning = 0;
+
+    function refreshRunning() {
+      $.getJSON(window.location.href + "?format=json&type=running", function (data) {
+        if (data) {
+          var nNodes = runningTable.fnGetNodes();
+
+          // check for zombie nodes
+          $(nNodes).each(function (iNode, node) {
+            var nodeFound = false;
+            $(data).each(function (iCoord, currentItem) {
+              if ($(node).children("td").eq(5).text() == currentItem.id) {
+                nodeFound = true;
+              }
+            });
+            if (!nodeFound) {
+              runningTable.fnDeleteRow(node);
+              runningTable.fnDraw();
+            }
+          });
+
+          $(data).each(function (iCoord, item) {
+            var coord = new Coordinator(item);
+            var foundRow = null;
+            $(nNodes).each(function (iNode, node) {
+              if ($(node).children("td").eq(5).text() == coord.id) {
+                foundRow = node;
+              }
+            });
+            if (foundRow == null) {
+              var killCell = "";
+              if (coord.canEdit) {
+                killCell = '<a class="btn btn-small confirmationModal" ' +
+                        'href="javascript:void(0)" ' +
+                        'data-url="' + coord.killUrl + '" ' +
+                        'title="${ _('Kill') } ' + coord.id + '"' +
+                        'alt="${ _('Are you sure you want to kill coordinator ')}' + coord.id + '?" ' +
+                        'data-message="${ _('The coordinator was killed!') }" ' +
+                        'data-confirmation-message="${ _('Are you sure you\'d like to kill this job?') }"' +
+                        '>${ _('Kill') }</a>';
+              }
+              if (['RUNNING', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED'].indexOf(coord.status) > -1) {
+                runningTable.fnAddData([coord.endTime, '<span class="' + coord.statusClass + '">' + coord.status + '</span>', coord.appName,
+                  '<div class="progress"><div class="' + coord.progressClass + '" style="width:' + coord.progress + '%">' + coord.progress + '%</div></div>', coord.user, '<a href="' + coord.absoluteUrl + '">' + coord.id + '</a>',
+                  killCell]);
+              }
+
+            }
+            else {
+              runningTable.fnUpdate('<span class="' + coord.statusClass + '">' + coord.status + '</span>', foundRow, 1, false);
+              runningTable.fnUpdate('<div class="progress"><div class="' + coord.progressClass + '" style="width:' + coord.progress + '%">' + coord.progress + '%</div></div>', foundRow, 3, false);
+            }
+          });
         }
         }
-      );
-      return false;
-    });
+        if (data.length == 0) {
+          runningTable.fnClearTable();
+        }
+
+        if (data.length != numRunning) {
+          refreshCompleted();
+        }
+        numRunning = data.length;
+
+        window.setTimeout(refreshRunning, 1000);
+      });
+    }
+
+    function refreshCompleted() {
+      $.getJSON(window.location.href + "?format=json&type=completed", function (data) {
+        completedTable.fnClearTable();
+        $(data).each(function (iWf, item) {
+          var coord = new Coordinator(item);
+          completedTable.fnAddData([coord.endTime, '<span class="' + coord.statusClass + '">' + coord.status + '</span>', coord.appName,
+            coord.duration, coord.user, '<a href="' + coord.absoluteUrl + '">' + coord.id + '</a>'], false);
+        });
+        completedTable.fnDraw();
+      });
+    }
 
 
   });
   });
 </script>
 </script>

+ 1 - 34
apps/oozie/src/oozie/templates/dashboard/list_oozie_workflow.mako

@@ -293,6 +293,7 @@ ${ layout.menubar(section='dashboard') }
   </div>
   </div>
 </div>
 </div>
 
 
+<script src="/oozie/static/js/utils.js" type="text/javascript" charset="utf-8"></script>
 <link rel="stylesheet" href="/oozie/static/css/workflow.css">
 <link rel="stylesheet" href="/oozie/static/css/workflow.css">
 <script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/knockout-2.1.0.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
@@ -311,40 +312,6 @@ ${ layout.menubar(section='dashboard') }
 
 
 <script type="text/javascript">
 <script type="text/javascript">
 
 
-  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'].indexOf(status) > -1){
-      klass = prefix + "success";
-    }
-    else if (['RUNNING', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED'].indexOf(status) > -1){
-      klass = prefix + "warning";
-    }
-    else if (status == 'READY'){
-      klass = prefix + "success";
-    }
-    else {
-      klass = prefix + "important";
-      if (prefix == "bar-"){
-        klass = prefix + "danger";
-      }
-    }
-    return klass;
-  }
-
-
   var Action = function (action) {
   var Action = function (action) {
     return {
     return {
       id: action.id,
       id: action.id,

+ 207 - 148
apps/oozie/src/oozie/templates/dashboard/list_oozie_workflows.mako

@@ -48,7 +48,7 @@ ${ layout.menubar(section='dashboard') }
         <span class="btn-group" style="float:left;">
         <span class="btn-group" style="float:left;">
           <a class="btn btn-status btn-success" data-value='SUCCEEDED'>${ _('Succeeded') }</a>
           <a class="btn btn-status btn-success" data-value='SUCCEEDED'>${ _('Succeeded') }</a>
           <a class="btn btn-status btn-warning" data-value='RUNNING'>${ _('Running') }</a>
           <a class="btn btn-status btn-warning" data-value='RUNNING'>${ _('Running') }</a>
-          <a class="btn btn-status btn-danger" data-value='KILLED'>${ _('Killed') }</a>
+          <a class="btn btn-status btn-danger disable-feedback" data-value='KILLED'>${ _('Killed') }</a>
         </span>
         </span>
       </span>
       </span>
    </form>
    </form>
@@ -69,35 +69,10 @@ ${ layout.menubar(section='dashboard') }
         </tr>
         </tr>
       </thead>
       </thead>
       <tbody>
       <tbody>
-        % for job in jobs['running_jobs']:
-          <tr>
-            <td>${ utils.format_date(job.lastModTime) }</td>
-            <td>
-              <span class="label ${ utils.get_status(job.status) }">
-                ${ job.status }
-              </span>
-            </td>
-            <td>${ job.appName }</td>
-            <td data-sort-value="${ job.get_progress() }">${ job.get_progress() }%</td>
-            <td>${ job.user }</td>
-            <td><a href="${ job.get_absolute_url() }" data-row-selector="true"></a>${ job.id }</td>
-            <td>
-              % if has_job_edition_permission(job, user):
-              <a title="${_('Kill %(workflow)s') % dict(workflow=job.id)}"
-                class="btn small confirmationModal"
-                alt="${ _('Are you sure you want to kill workflow %s?') %  job.id }"
-                href="javascript:void(0)"
-                data-url="${ url('oozie:manage_oozie_jobs', job_id=job.id, action='kill') }"
-                data-message="${ _('The workflow was killed!') }"
-                data-confirmation-message="${ _('Are you sure you\'d like to kill this job?') }">
-                  ${ _('Kill') }
-              </a>
-              % endif
-            </td>
-          </tr>
-        % endfor
+
       </tbody>
       </tbody>
     </table>
     </table>
+
   </div>
   </div>
 
 
   <div>
   <div>
@@ -114,28 +89,7 @@ ${ layout.menubar(section='dashboard') }
         </tr>
         </tr>
       </thead>
       </thead>
       <tbody>
       <tbody>
-        % for job in jobs['completed_jobs']:
-          <tr>
-            <td>${ utils.format_date(job.endTime) }</td>
-            <td>
-              <span class="label
-               % if job.status == 'SUCCEEDED':
-                 label-success
-               % elif job.is_running():
-                  label-warning
-               % else:
-                 label-important
-               % endif
-               ">
-                ${ job.status }
-              </span>
-            </td>
-            <td>${ job.appName }</td>
-            <td data-sort-value="${ utils.job_duration(job) }">${ utils.format_job_duration(job) }</td>
-            <td>${ job.user }</td>
-            <td><a href="${ job.get_absolute_url() }" data-row-selector="true"></a>${ job.id }</td>
-          </tr>
-        % endfor
+
       </tbody>
       </tbody>
      </table>
      </table>
    </div>
    </div>
@@ -153,100 +107,124 @@ ${ layout.menubar(section='dashboard') }
   </div>
   </div>
 </div>
 </div>
 
 
-
+<script src="/oozie/static/js/utils.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
 <script src="/static/ext/js/datatables-paging-0.1.js" type="text/javascript" charset="utf-8"></script>
 
 
 <script type="text/javascript" charset="utf-8">
 <script type="text/javascript" charset="utf-8">
-  $(document).ready(function() {
-    var runningTable = $('#running-table').dataTable( {
-      'sPaginationType': 'bootstrap',
-      'iDisplayLength': 50,
-      "bLengthChange": false,
-      "sDom": "<'row'r>t<'row'<'span6'i><''p>>",
-      "aoColumns": [
-           { "sType": "date" },
-            null,
-            null,
-            { "sSortDataType": "dom-sort-value", "sType": "numeric" },
-            null,
-            null,
-            { "bSortable": false }
-        ],
-        "aaSorting": [[ 0, "desc" ]],
-        "oLanguage": {
-            "sEmptyTable":     "${_('No data available')}",
-            "sInfo":           "${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
-            "sInfoEmpty":      "${_('Showing 0 to 0 of 0 entries')}",
-            "sInfoFiltered":   "${_('(filtered from _MAX_ total entries)')}",
-            "sZeroRecords":    "${_('No matching records')}",
-            "oPaginate": {
-                "sFirst":    "${_('First')}",
-                "sLast":     "${_('Last')}",
-                "sNext":     "${_('Next')}",
-                "sPrevious": "${_('Previous')}"
-            }
+
+  var Workflow = function (wf) {
+    return {
+      id:wf.id,
+      lastModTime:wf.lastModTime,
+      endTime:wf.endTime,
+      status:wf.status,
+      statusClass:"label " + getStatusClass(wf.status),
+      isRunning:wf.isRunning,
+      duration:wf.duration,
+      appName:wf.appName,
+      progress:wf.progress,
+      progressClass:"bar " + getStatusClass(wf.status, "bar-"),
+      user:wf.user,
+      absoluteUrl:wf.absoluteUrl,
+      canEdit:wf.canEdit,
+      killUrl:wf.killUrl
+    }
+  }
+
+  $(document).ready(function () {
+    var runningTable = $('#running-table').dataTable({
+      'sPaginationType':'bootstrap',
+      'iDisplayLength':50,
+      "bLengthChange":false,
+      "sDom":"<'row'r>t<'row'<'span6'i><''p>>",
+      "aoColumns":[
+        { "sType":"date" },
+        null,
+        null,
+        { "sSortDataType":"dom-sort-value", "sType":"numeric" },
+        null,
+        null,
+        { "bSortable":false }
+      ],
+      "aaSorting":[
+        [ 0, "desc" ]
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sInfo":"${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
+        "sInfoEmpty":"${_('Showing 0 to 0 of 0 entries')}",
+        "sInfoFiltered":"${_('(filtered from _MAX_ total entries)')}",
+        "sZeroRecords":"${_('No matching records')}",
+        "oPaginate":{
+          "sFirst":"${_('First')}",
+          "sLast":"${_('Last')}",
+          "sNext":"${_('Next')}",
+          "sPrevious":"${_('Previous')}"
         }
         }
+      }
     });
     });
 
 
-    var completedTable = $('#completed-table').dataTable( {
-      'sPaginationType': 'bootstrap',
-      'iDisplayLength': 50,
-      "bLengthChange": false,
-      "sDom": "<'row'r>t<'row'<'span6'i><''p>>",
-      "aoColumns": [
-            { "sType": "date" },
-            null,
-            null,
-            { "sSortDataType": "dom-sort-value", "sType": "numeric" },
-            null,
-            null
-        ],
-        "aaSorting": [[ 0, "desc" ]],
-        "oLanguage": {
-            "sEmptyTable":     "${_('No data available')}",
-            "sInfo":           "${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
-            "sInfoEmpty":      "${_('Showing 0 to 0 of 0 entries')}",
-            "sInfoFiltered":   "${_('(filtered from _MAX_ total entries)')}",
-            "sZeroRecords":    "${_('No matching records')}",
-            "oPaginate": {
-                "sFirst":    "${_('First')}",
-                "sLast":     "${_('Last')}",
-                "sNext":     "${_('Next')}",
-                "sPrevious": "${_('Previous')}"
-            }
+    var completedTable = $('#completed-table').dataTable({
+      'sPaginationType':'bootstrap',
+      'iDisplayLength':50,
+      "bLengthChange":false,
+      "sDom":"<'row'r>t<'row'<'span6'i><''p>>",
+      "aoColumns":[
+        { "sType":"date" },
+        null,
+        null,
+        { "sSortDataType":"dom-sort-value", "sType":"numeric" },
+        null,
+        null
+      ],
+      "aaSorting":[
+        [ 0, "desc" ]
+      ],
+      "oLanguage":{
+        "sEmptyTable":"${_('No data available')}",
+        "sInfo":"${_('Showing _START_ to _END_ of _TOTAL_ entries')}",
+        "sInfoEmpty":"${_('Showing 0 to 0 of 0 entries')}",
+        "sInfoFiltered":"${_('(filtered from _MAX_ total entries)')}",
+        "sZeroRecords":"${_('No matching records')}",
+        "oPaginate":{
+          "sFirst":"${_('First')}",
+          "sLast":"${_('Last')}",
+          "sNext":"${_('Next')}",
+          "sPrevious":"${_('Previous')}"
         }
         }
+      }
     });
     });
 
 
-    $('#filterInput').keydown(function(e) {
+    $('#filterInput').keydown(function (e) {
       if (e.which == 13) {
       if (e.which == 13) {
         e.preventDefault();
         e.preventDefault();
         return false;
         return false;
       }
       }
     });
     });
 
 
-    $("#filterInput").keyup(function() {
+    $("#filterInput").keyup(function () {
       runningTable.fnFilter($(this).val());
       runningTable.fnFilter($(this).val());
       completedTable.fnFilter($(this).val());
       completedTable.fnFilter($(this).val());
       drawTable();
       drawTable();
     });
     });
 
 
 
 
-    $("a.btn-status").click(function() {
+    $("a.btn-status").click(function () {
       $(this).toggleClass('active');
       $(this).toggleClass('active');
       drawTable();
       drawTable();
     });
     });
 
 
-    $("a.btn-date").click(function() {
+    $("a.btn-date").click(function () {
       $("a.btn-date").not(this).removeClass('active');
       $("a.btn-date").not(this).removeClass('active');
       $(this).toggleClass('active');
       $(this).toggleClass('active');
       drawTable();
       drawTable();
     });
     });
 
 
-    function drawTable(){
+    function drawTable() {
       runningTable.fnDraw();
       runningTable.fnDraw();
       completedTable.fnDraw();
       completedTable.fnDraw();
 
 
-      hash = "#";
+      var hash = "#";
       if ($("a.btn-date.active").length > 0) {
       if ($("a.btn-date.active").length > 0) {
         hash += "date=" + $("a.btn-date.active").text();
         hash += "date=" + $("a.btn-date.active").text();
       }
       }
@@ -254,57 +232,138 @@ ${ layout.menubar(section='dashboard') }
     }
     }
 
 
     $.fn.dataTableExt.afnFiltering.push(
     $.fn.dataTableExt.afnFiltering.push(
-      function(oSettings, aData, iDataIndex) {
-        urlHashes = ""
-
-        statusBtn = $('a.btn-status.active');
-        statusFilter = true;
-        if (statusBtn.length > 0) {
-          statuses = []
-          $.each(statusBtn, function() {
-            statuses.push($(this).attr('data-value'));
-          });
-          statusFilter = aData[1].match(RegExp(statuses.join('|'), "i")) != null;
-        }
-
-        dateBtn = $('a.btn-date.active');
-        dateFilter = true;
-        if (dateBtn.length > 0) {
-          minAge = new Date() - parseInt(dateBtn.attr('data-value')) * 1000 * 60 * 60 * 24;
-          dateFilter = Date.parse(aData[0]) >= minAge;
-        }
-
-        return statusFilter && dateFilter;
-      }
+            function (oSettings, aData, iDataIndex) {
+              var urlHashes = ""
+
+              var statusBtn = $('a.btn-status.active');
+              var statusFilter = true;
+              if (statusBtn.length > 0) {
+                var statuses = []
+                $.each(statusBtn, function () {
+                  statuses.push($(this).attr('data-value'));
+                });
+                statusFilter = aData[1].match(RegExp(statuses.join('|'), "i")) != null;
+              }
+
+              var dateBtn = $('a.btn-date.active');
+              var dateFilter = true;
+              if (dateBtn.length > 0) {
+                var minAge = new Date() - parseInt(dateBtn.attr('data-value')) * 1000 * 60 * 60 * 24;
+                dateFilter = Date.parse(aData[0]) >= minAge;
+              }
+
+              return statusFilter && dateFilter;
+            }
     );
     );
 
 
     $("a[data-row-selector='true']").jHueRowSelector();
     $("a[data-row-selector='true']").jHueRowSelector();
 
 
-    $(".confirmationModal").click(function(){
+    $(document).on("click", ".confirmationModal", function () {
       var _this = $(this);
       var _this = $(this);
+      _this.bind('confirmation', function () {
+        var _this = this;
+        $.post($(this).attr("data-url"),
+                { 'notification':$(this).attr("data-message") },
+                function (response) {
+                  if (response['status'] != 0) {
+                    $.jHueNotify.error("${ _('Problem: ') }" + response['data']);
+                  } else {
+                    window.location.reload();
+                  }
+                }
+        );
+        return false;
+      });
       $("#confirmation .message").text(_this.attr("data-confirmation-message"));
       $("#confirmation .message").text(_this.attr("data-confirmation-message"));
       $("#confirmation").modal("show");
       $("#confirmation").modal("show");
-      $("#confirmation a.btn-danger").click(function() {
+      $("#confirmation a.btn-danger").click(function () {
         _this.trigger('confirmation');
         _this.trigger('confirmation');
       });
       });
     });
     });
 
 
-    $(".confirmationModal").bind('confirmation', function() {
-      var _this = this;
-      $.post($(this).attr("data-url"),
-        { 'notification': $(this).attr("data-message") },
-        function(response) {
-          if (response['status'] != 0) {
-            $.jHueNotify.error("${ _('Problem: ') }" + response['data']);
-          } else {
-            window.location.reload();
-          }
+    refreshRunning();
+    refreshCompleted();
+
+    var numRunning = 0;
+
+    function refreshRunning() {
+      $.getJSON(window.location.href + "?format=json&type=running", function (data) {
+        if (data) {
+          var nNodes = runningTable.fnGetNodes();
+
+          // check for zombie nodes
+          $(nNodes).each(function (iNode, node) {
+            var nodeFound = false;
+            $(data).each(function (iWf, currentItem) {
+              if ($(node).children("td").eq(5).text() == currentItem.id) {
+                nodeFound = true;
+              }
+            });
+            if (!nodeFound) {
+              runningTable.fnDeleteRow(node);
+              runningTable.fnDraw();
+            }
+          });
+
+          $(data).each(function (iWf, item) {
+            var wf = new Workflow(item);
+            var foundRow = null;
+            $(nNodes).each(function (iNode, node) {
+              if ($(node).children("td").eq(5).text() == wf.id) {
+                foundRow = node;
+              }
+            });
+            if (foundRow == null) {
+              var killCell = "";
+              if (wf.canEdit) {
+                killCell = '<a class="btn btn-small confirmationModal" ' +
+                        'href="javascript:void(0)" ' +
+                        'data-url="' + wf.killUrl + '" ' +
+                        'title="${ _('Kill') } ' + wf.id + '"' +
+                        'alt="${ _('Are you sure you want to kill workflow ')}' + wf.id + '?" ' +
+                        'data-message="${ _('The workflow was killed!') }" ' +
+                        'data-confirmation-message="${ _('Are you sure you\'d like to kill this job?') }"' +
+                        '>${ _('Kill') }</a>';
+              }
+              if (['RUNNING', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED'].indexOf(wf.status) > -1) {
+                runningTable.fnAddData([wf.lastModTime, '<span class="' + wf.statusClass + '">' + wf.status + '</span>', wf.appName,
+                  '<div class="progress"><div class="' + wf.progressClass + '" style="width:' + wf.progress + '%">' + wf.progress + '%</div></div>', wf.user, '<a href="' + wf.absoluteUrl + '">' + wf.id + '</a>',
+                  killCell]);
+              }
+            }
+            else {
+              runningTable.fnUpdate('<span class="' + wf.statusClass + '">' + wf.status + '</span>', foundRow, 1, false);
+              runningTable.fnUpdate('<div class="progress"><div class="' + wf.progressClass + '" style="width:' + wf.progress + '%">' + wf.progress + '%</div></div>', foundRow, 3, false);
+            }
+          });
+        }
+        if (data.length == 0) {
+          runningTable.fnClearTable();
         }
         }
-      );
-      return false;
-    });
+
+        if (data.length != numRunning) {
+          refreshCompleted();
+        }
+        numRunning = data.length;
+
+        window.setTimeout(refreshRunning, 1000);
+      });
+    }
+
+    function refreshCompleted() {
+      $.getJSON(window.location.href + "?format=json&type=completed", function (data) {
+        completedTable.fnClearTable();
+        $(data).each(function (iWf, item) {
+          var wf = new Workflow(item);
+          completedTable.fnAddData([wf.endTime, '<span class="' + wf.statusClass + '">' + wf.status + '</span>', decodeURIComponent(wf.appName),
+            wf.duration, wf.user, '<a href="' + wf.absoluteUrl + '">' + wf.id + '</a>'], false);
+        });
+        completedTable.fnDraw();
+      });
+    }
 
 
   });
   });
+
 </script>
 </script>
 
 
 ${ commonfooter(messages) | n,unicode }
 ${ commonfooter(messages) | n,unicode }

+ 21 - 6
apps/oozie/src/oozie/views/dashboard.py

@@ -43,6 +43,7 @@ from oozie.models import History, Job, Workflow
 from oozie.settings import DJANGO_APPS
 from oozie.settings import DJANGO_APPS
 
 
 from django.template.defaultfilters import escapejs
 from django.template.defaultfilters import escapejs
+from desktop.lib.view_util import format_duration_in_millis
 
 
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
@@ -102,7 +103,12 @@ def list_oozie_workflows(request):
   workflows = get_oozie().get_workflows(**kwargs)
   workflows = get_oozie().get_workflows(**kwargs)
 
 
   if request.GET.get('format') == 'json':
   if request.GET.get('format') == 'json':
-    return HttpResponse(json.dumps(massaged_oozie_jobs_for_json(workflows.jobs, request.user)), mimetype="application/json")
+    json_jobs = workflows.jobs
+    if request.GET.get('type') == 'running':
+      json_jobs = split_oozie_jobs(workflows.jobs)['running_jobs']
+    if request.GET.get('type') == 'completed':
+      json_jobs = split_oozie_jobs(workflows.jobs)['completed_jobs']
+    return HttpResponse(json.dumps(massaged_oozie_jobs_for_json(json_jobs, request.user)).replace('\\\\', '\\'), mimetype="application/json")
 
 
   return render('dashboard/list_oozie_workflows.mako', request, {
   return render('dashboard/list_oozie_workflows.mako', request, {
     'user': request.user,
     'user': request.user,
@@ -119,6 +125,14 @@ def list_oozie_coordinators(request):
 
 
   coordinators = get_oozie().get_coordinators(**kwargs)
   coordinators = get_oozie().get_coordinators(**kwargs)
 
 
+  if request.GET.get('format') == 'json':
+    json_jobs = coordinators.jobs
+    if request.GET.get('type') == 'running':
+      json_jobs = split_oozie_jobs(coordinators.jobs)['running_jobs']
+    if request.GET.get('type') == 'completed':
+      json_jobs = split_oozie_jobs(coordinators.jobs)['completed_jobs']
+    return HttpResponse(json.dumps(massaged_oozie_jobs_for_json(json_jobs, request.user)).replace('\\\\', '\\'), mimetype="application/json")
+
   return render('dashboard/list_oozie_coordinators.mako', request, {
   return render('dashboard/list_oozie_coordinators.mako', request, {
     'jobs': split_oozie_jobs(coordinators.jobs),
     'jobs': split_oozie_jobs(coordinators.jobs),
     'has_job_edition_permission': has_job_edition_permission,
     'has_job_edition_permission': has_job_edition_permission,
@@ -157,7 +171,7 @@ def list_oozie_workflow(request, job_id, coordinator_job_id=None):
       'log': oozie_workflow.log,
       'log': oozie_workflow.log,
       'actions': massaged_workflow_actions_for_json(oozie_workflow.get_working_actions())
       'actions': massaged_workflow_actions_for_json(oozie_workflow.get_working_actions())
     }
     }
-    return HttpResponse(json.dumps(return_obj), mimetype="application/json")
+    return HttpResponse(json.dumps(return_obj).replace('\\\\', '\\'), mimetype="application/json")
 
 
   return render('dashboard/list_oozie_workflow.mako', request, {
   return render('dashboard/list_oozie_workflow.mako', request, {
     'history': history,
     'history': history,
@@ -192,7 +206,7 @@ def list_oozie_coordinator(request, job_id):
       'log': oozie_coordinator.log,
       'log': oozie_coordinator.log,
       'actions': massaged_coordinator_actions_for_json(oozie_coordinator)
       'actions': massaged_coordinator_actions_for_json(oozie_coordinator)
     }
     }
-    return HttpResponse(json.dumps(return_obj), mimetype="application/json")
+    return HttpResponse(json.dumps(return_obj).replace('\\\\', '\\'), mimetype="application/json")
 
 
   return render('dashboard/list_oozie_coordinator.mako', request, {
   return render('dashboard/list_oozie_coordinator.mako', request, {
     'oozie_coordinator': oozie_coordinator,
     'oozie_coordinator': oozie_coordinator,
@@ -388,16 +402,17 @@ def massaged_oozie_jobs_for_json(oozie_jobs, user):
 
 
     massaged_job = {
     massaged_job = {
       'id': job.id,
       'id': job.id,
-      'lastModTime': time.mktime(job.lastModTime),
-      'endTime': time.mktime(job.endTime),
+      'lastModTime': hasattr(job, 'lastModTime') and job.lastModTime and format_time(job.lastModTime) or None,
+      'endTime': job.endTime and format_time(job.endTime) or None,
       'status': job.status,
       'status': job.status,
       'isRunning': job.is_running(),
       'isRunning': job.is_running(),
-      'duration': job.endTime and job.startTime and ( time.mktime(job.endTime) - time.mktime(job.startTime) ) * 1000 or None,
+      'duration': job.endTime and job.startTime and format_duration_in_millis(( time.mktime(job.endTime) - time.mktime(job.startTime) ) * 1000) or None,
       'appName': escapejs(job.appName),
       'appName': escapejs(job.appName),
       'progress': job.get_progress(),
       'progress': job.get_progress(),
       'user': job.user,
       'user': job.user,
       'absoluteUrl': job.get_absolute_url(),
       'absoluteUrl': job.get_absolute_url(),
       'canEdit': has_job_edition_permission(job, user),
       'canEdit': has_job_edition_permission(job, user),
+      'killUrl': reverse('oozie:manage_oozie_jobs', kwargs={'job_id':job.id, 'action':'kill'}),
       }
       }
     jobs.append(massaged_job)
     jobs.append(massaged_job)
 
 

+ 48 - 0
apps/oozie/static/js/utils.js

@@ -0,0 +1,48 @@
+// Licensed to Cloudera, Inc. under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  Cloudera, Inc. licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+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'].indexOf(status) > -1){
+    klass = prefix + "success";
+  }
+  else if (['RUNNING', 'PREP', 'WAITING', 'SUSPENDED', 'PREPSUSPENDED', 'PREPPAUSED', 'PAUSED'].indexOf(status) > -1){
+    klass = prefix + "warning";
+  }
+  else if (status == 'READY'){
+    klass = prefix + "success";
+  }
+  else {
+    klass = prefix + "important";
+    if (prefix == "bar-"){
+      klass = prefix + "danger";
+    }
+  }
+  return klass;
+}

+ 3 - 1
desktop/core/src/desktop/templates/common_footer.html

@@ -32,7 +32,9 @@ limitations under the License.
     <script type="text/javascript">
     <script type="text/javascript">
       $(document).ready(function () {
       $(document).ready(function () {
         $(".dataTables_wrapper").jHueTableScroller();
         $(".dataTables_wrapper").jHueTableScroller();
-        $(document).on("click", ".btn-primary, .btn-danger", function () {
+        // sets feedback on every primary and danger action across Hue
+        // can be disabled adding the class "disable-feedback" to the button
+        $(document).on("click", ".btn-primary:not(.disable-feedback), .btn-danger:not(.disable-feedback)", function () {
           if (!$(this).hasClass('disabled')) {
           if (!$(this).hasClass('disabled')) {
             var text = ($(this).text()) ? $(this).text() : $(this).val();
             var text = ($(this).text()) ? $(this).text() : $(this).val();
             $(this).attr("data-loading-text", text + " ...");
             $(this).attr("data-loading-text", text + " ...");